Forbidden keys in clojure.spec
Asked Answered
C

2

16

I am following the clojure.spec guide. I understand it is possible to declare required and optional attributes when using clojure.spec/keys.

I don't understand what is meant by optional. To me :opt doesn't do anything.

(s/valid? (s/keys :req [:my/a]) {:my/a 1 :my/b 2}) ;=> true

(s/valid? (s/keys :req [:my/a] :opt []) {:my/a 1 :my/b 2}) ;=> true

The guide promises to explain this to me, "We’ll see later where optional attributes can be useful", but I fail to find the explanation. Can I declare forbidden keys? Or somehow declare the set of valid keys to equal the keys in :req and :opt?

Centavo answered 30/6, 2016 at 18:59 Comment(0)
D
13

This is a very good question, and the clojure.spec API gives the (granted, short and unsatisfying) answer:

The :opt keys serve as documentation and may be used by the generator.

I do not think you can invalidate a map if it contains an extra (this is what you mean by "forbidden" I think) key using this method. However, you could use this spec to make sure ::bad-key is not present:

(s/def ::m (s/and (s/keys :req [::a]) #(not (contains? % ::bad-key))))
(s/valid? ::m {::a "required!"})                        ; => true
(s/valid? ::m {::a "required!" ::b "optional!"})        ; => true
(s/valid? ::m {::a "required!" ::bad-key "no good!"})   ; => false

You could limit the number of keys to exactly the set you want by using this spec:

(s/def ::r (s/and (s/keys :req [::reqd1 ::reqd2]) #(= (count %) 2)))
(s/valid? ::r {::reqd1 "abc" ::reqd2 "xyz"})              ; => true
(s/valid? ::r {::reqd1 "abc" ::reqd2 "xyz" ::extra 123})  ; => false

Still, the best way to handle this IMO, would be to simply ignore that there is a key present that you don't care about.

Hopefully as spec matures, these nice things will be added. Or, maybe they are already there (it is changing rapidly) and I simply don't know about it. This is a very new concept in clojure, so most of us have a lot to learn about it.

UPDATE - December 2016 I just wanted to revisit this 6 months since writing it. It looks like my initial comment about ignoring keys you don't care about is the preferred way to go. In fact, at the clojure/conj conference I attended two weeks ago, Rich's keynote specifically addressed the notion of versioning in all levels of software, from the function level up to the application level. He even specifically mentions this notion of disallowing keys in the talk, which can be found on youtube. He says that it was intentionally designed so that only required keys can be spec'd. Disallowing keys really serves no good purpose, and it should be done with caution.

Regarding the :opt keys, I think the original answer still stands up pretty well--it's documentation, and practically, it allows these optionally specified keys to be generated:

(s/def ::name #{"Bob" "Josh" "Mary" "Susan"})
(s/def ::height-inches (s/int-in 48 90))
(s/def ::person (s/keys :req-un [::name] :opt-un [::height-inches]))

(map first (s/exercise ::person))

; some generated data have :height-inches, some do not
({:name "Susan"}
 {:name "Mary", :height-inches 48}
 {:name "Bob", :height-inches 49}
 {:name "Josh"}
Dovelike answered 30/6, 2016 at 19:24 Comment(4)
"Disallowing keys really serves no good purpose, and it should be done with caution." Hmm. I'll have to watch the talk to hear that in context but what about, say, checking that a password field doesn't leak out of a function? I guess the point is that you cannot really ensure you're not leaking it in some other way besides an explicit key, but it seems a bit overstated to say that checking for the obvious has no good purpose, doesn't it?Wallet
So I listened to the talk. I see that spec is about what you can, not what you can't, do. But he didn't exactly say that ignoring it was the preferred way. He said it was one of two preferred ways, the other being "have a policy". If I understand that correctly, I could do a couple of things to not block growth: 1) define a separately named spec from the "just ignore" one that disallowed the key using the method you show, which I can use now, maybe keep using forever, or drop later, 2) write independent checkers. He actually addressed my case too: maybe you should use select-keys.Wallet
Here's another use case to consider: I'm parsing out data from a binary protocol. If one of the keys in the data's header matches a certain value, then there should be an additional field parsed out of the bits following the header. However, if the header doesn't match that value, then it would be very bad for the subsequent bits to have been consumed into this optional extra field. In this case, it seems to me that I do want to forbid this key under this condition, because I want to know if I parsed the packet wrong.Deandeana
(I'll add that the optional key's value could also conceivably be nil, but I still need to enforce that it's either nil or not present.)Deandeana
C
-3

The point about optional keys is that the value will be validated if they appear in the map

Cement answered 30/6, 2016 at 21:25 Comment(4)
That is untrue. The value will be validated regardless.Pyrimidine
If the key is neither required or optional, the value will not be validated at all. I did not meant that for required keys the value was not validatedCement
"In addition, the values of all namespace-qualified keys will be validated (and possibly destructured) by any registered specs." (Docstr. of s/keys)Pyrimidine
Ouch, it was so clear in my mind. Will leave the answer as reference. Thanks!Cement

© 2022 - 2024 — McMap. All rights reserved.