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?

Johan Jonasson
  • 500
  • 2
  • 18

2 Answers2

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"}
Josh
  • 4,726
  • 2
  • 20
  • 32
  • 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? – neverfox Aug 29 '17 at 13:55
  • 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. – neverfox Aug 29 '17 at 15:01
  • 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. – WhittlesJr Oct 10 '19 at 16:09
  • (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.) – WhittlesJr Oct 10 '19 at 16:31
-3

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

DanLebrero
  • 8,545
  • 1
  • 29
  • 30