Maybe we can do a little more: (defmacro limit-keys [& {:keys [req req-un opt opt-un only] :as args}] (if only `(s/merge (s/keys ~@(apply concat (vec args))) (s/map-of ~(set (concat req (map (comp keyword name) req-un) opt (map (comp keyword name) opt-un))) any?))
`(s/keys ~@(apply concat (vec args))))) Test Code like this: (def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") (s/def ::email-type (s/and string? #(re-matches email-regex %))) (def phone-regex #"^[0-9]{7,11}$") (s/def ::phone (s/and string? #(re-matches phone-regex %))) (s/def ::acctid int?) (s/def ::first-name string?) (s/def ::last-name string?) (s/def ::email ::email-type) (s/def ::person (limit-keys :req [::first-name ::last-name ::email] :opt [::phone])) (s/def ::x-person (limit-keys :req [::first-name ::last-name ::email] :opt [::phone] :only true)) (deftest smart-test (is (= true (s/valid? ::person {::first-name "Bugs" ::last-name "Bunny" ::email "b...@example.com"}))) (is (= true (s/valid? ::person {::first-name "Bugs" ::last-name "Bunny" ::email "b...@example.com" ::phone "1000000"}))) (is (= false (s/valid? ::x-person {::first-name "Bugs" ::last-name "Bunny" ::email "b...@example.com" ::x-phone "1000000"}))) (is (= true (s/valid? ::person {::first-name "Bugs" ::last-name "Bunny" ::email "b...@example.com" ::x-phone "1000000"})))) 在 2016年9月25日星期日 UTC+8下午3:15:12,Alistair Roche写道: > > Whoops, that should be: > > (defmacro only-keys > [& {:keys [req req-un opt opt-un] :as args}] > `(s/merge (s/keys ~@(apply concat (vec args))) > (s/map-of ~(set (concat req > (map (comp keyword name) req-un) > opt > (map (comp keyword name) opt-un))) > any?))) > > > > On Sunday, 25 September 2016 17:13:12 UTC+10, Alistair Roche wrote: >> >> Yeah, my team and I were initially surprised at the lack of a built-in >> option for this to s/keys, but TBH it's been an unusual use case so far, >> and Alex / Beau's solutions don't seem particularly onerous despite the >> repetition. >> >> I suppose if you're using it all over the place you could write a macro >> like this: >> >> >> (defmacro only-keys >> [& {:keys [req req-un opt opt-un] :as args}] >> `(s/and (s/keys ~@(apply concat (vec args))) >> (s/map-of ~(set (concat req >> (map (comp keyword name) req-un) >> opt >> (map (comp keyword name) opt-un))) >> any?))) >> >> >> >> (please feel free to suggest a neater way!) >> >> Cheers, >> >> On Wednesday, 21 September 2016 18:08:25 UTC+10, David Goldfarb wrote: >>> >>> Nice, thanks. I had not thought to use map-of for this. And, s/merge >>> certainly helps too. >>> >>> The only remaining issue for me is that this requires supplying the list >>> of keys twice. >>> AI think this case is general enough that it is worth extending the >>> s/keys macro to support: (s/keys :req [::a ::b] :allow-other-keys false) >>> >>> Or, if is is objectionable to have a keyword default to true when not >>> supplied, perhaps: (s/keys :req [::a ::b] :strict-keys true) >>> >>> On Tuesday, September 20, 2016 at 9:47:43 PM UTC+3, Alex Miller wrote: >>>> >>>> For stuff like this s/merge is probably preferable to s/and (when >>>> combining map specs) - the difference being that merge does not flow >>>> conformed results, will combine all failures, and that gen can work better >>>> in some cases. >>>> >>>> (s/def ::a int?) >>>> (s/def ::b string?) ;; changed for this example >>>> (s/explain ::my-map {::a 1 ::b 2 ::BAD 3}) >>>> In: [:user/b] val: 2 fails spec: :user/b at: [:user/b] predicate: >>>> string? >>>> >>>> ;; vs: >>>> >>>> (s/def ::my-map2 (s/merge (s/keys :req [::a ::b]) (s/map-of #{::a ::b} >>>> any?))) >>>> (s/explain ::my-map2 {::a 1 ::b 2 ::BAD 3}) >>>> In: [:user/b] val: 2 fails spec: :user/b at: [:user/b] predicate: >>>> string? >>>> In: [:user/BAD 0] val: :user/BAD fails spec: :user/my-map2 at: [0] >>>> predicate: #{:user/a :user/b} >>>> >>>> ^^ Note you get *both* failures here - both bad attribute value AND the >>>> invalid key vs the prior one where you only get the first failure. >>>> >>>> >>>> On Tuesday, September 20, 2016 at 11:38:47 AM UTC-5, Beau Fabry wrote: >>>>> >>>>> boot.user=> (s/def ::my-map (s/and (s/keys :req [::a ::b]) (s/map-of >>>>> #{::a ::b} any?))) >>>>> boot.user=> (s/explain ::my-map {::a 1 ::b 2 ::BAD 3}) >>>>> In: [:boot.user/BAD 0] val: :boot.user/BAD fails spec: >>>>> :boot.user/my-map at: [0] predicate: #{:boot.user/a :boot.user/b} >>>>> >>>>> Seems better >>>>> >>>>> On Tuesday, September 20, 2016 at 5:38:10 AM UTC-7, David Goldfarb >>>>> wrote: >>>>>> >>>>>> In clojure.spec, how can I declare a map that accepts only certain >>>>>> keys? >>>>>> >>>>>> *{::a 1 ::b 2 ::BAD 3}* does conform to *(s/keys :req :req [::a >>>>>> ::b])*, but I want a spec that will be bothered by ::BAD or any >>>>>> other undeclared key. >>>>>> >>>>>> My use case: I am introducing spec to some legacy code, and I want to >>>>>> be warned if I have failed to specify some elements that may appear in >>>>>> my >>>>>> map. >>>>>> >>>>>> >>>>>> Question 2: >>>>>> >>>>>> So, assuming that this is not possible currently, I brute-forced it >>>>>> with: >>>>>> >>>>>> *(defn- key-checker [valid-keys]* >>>>>> * (fn [map-to-check]* >>>>>> * (empty? (clojure.set/difference (into #{} (keys map-to-check)) >>>>>> valid-keys))))* >>>>>> >>>>>> *(s/def ::my-map (s/and (s/keys :req [::a ::b]) (key-checker #{::a >>>>>> ::b})))* >>>>>> >>>>>> >>>>>> Ignoring the ugly, and easily fixable, smell of the duplicated set of >>>>>> keys, this has a bigger problem: >>>>>> >>>>>> If the predicate fails, the error that assert gives me is *"{... big >>>>>> ugly map ...} fails predicate: (key-checker #{::a ::b})"* with no >>>>>> easy way for the viewer to see which key failed. Can I somehow hook into >>>>>> the explain mechanism to give a more useful message? >>>>>> >>>>> -- You received this message because you are subscribed to the Google Groups "Clojure" group. To post to this group, send email to clojure@googlegroups.com Note that posts from new members are moderated - please be patient with your first post. To unsubscribe from this group, send email to clojure+unsubscr...@googlegroups.com For more options, visit this group at http://groups.google.com/group/clojure?hl=en --- You received this message because you are subscribed to the Google Groups "Clojure" group. To unsubscribe from this group and stop receiving emails from it, send an email to clojure+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.