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.

Reply via email to