mmer
mmer

Reputation: 35

clojure spec - validating contents of maps

I want to create a clojure spec for a map that has rules about the presence of particular keys.

The map must have a :type and can have either :default or :value but not both. I tried:

(s/def ::propertyDef
  (s/keys :req [::type (s/or ::default ::value) ] :opt [::description ::required]))

but I got

CompilerException java.lang.AssertionError: Assert failed:
    spec/or expects k1 p1 k2 p2..., where ks are keywords
    (c/and (even? (count key-pred-forms)) (every? keyword? keys)),
    compiling:(C:\Users\MartinRoberts\AppData\Local\Temp\form-init4830956164341520551.clj:1:22) 

but the or gave me an error as it is in the wrong format. I have to admit to not really understanding in the documentation for s/or.

Upvotes: 2

Views: 1678

Answers (1)

Josh
Josh

Reputation: 4806

First: you are using s/or to specify either a ::default or a ::value in your list of required keys. s/or requires :label spec pairs, and you are giving only the specs themselves, which is the cause of the error.

To solve, simply use or instead:

(s/def ::propertyDef (s/keys :req [::type (or ::default ::value)]
                             :opt [::description ::required]))

This allows both ::default and ::value to be present in the map, but this is almost always okay. The code which actually uses the map can simply check for the presence of ::value and use that, and if it's not there, then use ::default (or whatever your logic happens to be). This is usually done as such:

(let [myvalue (or (::value mymap) (::default mymap))] ...)

There could be thousands of keys in the map, and it would not affect your ability to extract the keys you need. This is why spec does not provide a built-in way to specify keys that should not be in the map, only ways to specify which keys should be present (namely, :req and :req-un in s/keys). Think of how most http servers work: you can give them nonsensical header keys and values, but they don't refuse to service the request; they just ignore them and return a response.

So, you likely don't need to enforce that only one or the other be present, but if you must, you can define an exclusive or function:

(defn xor
  [p q]
  (and (or p q)
       (not (and p q))))

and then add this as an additional predicate on the spec:

(s/def ::propertyDef (s/and (s/keys :req [::type (or ::default ::value)]
                                    :opt [::description ::required])
                            #(xor (::default %) (::value %))))

(s/valid? ::propertyDef {::type "type" ::default "default"})
=> true
(s/valid? ::propertyDef {::type "type" ::value "value"})
=> true
(s/valid? ::propertyDef {::type "type" ::default "default" ::value "value"})
=> false

Upvotes: 6

Related Questions