clojure spec: map containing either a :with or a :height (XOR)
Asked Answered
W

3

7

The following clojure spec ::my permits maps having either the key :width or the key :height, however it does not permit having both of them:

(s/def ::width int?)

(s/def ::height int?)

(defn one-of-both? [a b]
  (or (and a (not b))
      (and b (not a))))

(s/def ::my (s/and (s/keys :opt-un [::width ::height])
                   #(one-of-both? (% :width) (% :height))))

Even if it does the job:

(s/valid? ::my {})
false
(s/valid? ::my {:width 5})
true
(s/valid? ::my {:height 2})
true
(s/valid? ::my {:width 5 :height 2})
false

the code does not appear that concise to me. First the keys are defined as optional and then as required. Does anyone have a more readable solution to this?

Wearproof answered 27/1, 2017 at 17:16 Comment(1)
Just wanted to point out that the above logic fails if the value held by any of the keys are falsey, i.e. false or nil: (spec/valid? ::my {:width nil}) => false.Ectoparasite
V
10

clojure.spec is designed to encourage specs capable of growth. Thus its s/keys does not support forbidding keys. It even matches maps having keys that are neither in :req or opt.

There is however a way to say the map must at least have :width or :height, i.e. not XOR, just OR.

(s/def ::my (s/keys :req-un [(or ::width ::height)]))
Verlinevermeer answered 27/1, 2017 at 19:27 Comment(0)
G
6

This feature is built into spec - you can specify and/or patterns in req-un:

(s/def ::my (s/keys :req-un [(or ::width ::height)]))
:user/my
user=> (s/valid? ::my {})
false
user=> (s/valid? ::my {:width 5})
true
user=> (s/valid? ::my {:height 2})
true
user=> (s/valid? ::my {:width 5 :height 2})
true
Gangplank answered 27/1, 2017 at 19:29 Comment(0)
E
1

Just wanted to pitch in with a small modification to the spec in the original question which logic will fail if the value held by any key is falsey, i.e. false or nil.

(spec/valid? ::my {:width nil})
=> false

Of course this won't occur with the given constraints of int? put on the values in the question. But perhaps someone in posterity allows their values to be nilable or boolean in which case this answer becomes handy.

If we instead define the spec as:

(defn xor? [coll a-key b-key]
  (let [a (contains? coll a-key)
        b (contains? coll b-key)]
    (or (and a (not b))
        (and b (not a)))))

(spec/def ::my (spec/and (spec/keys :opt-un [::width ::height])
                         #(xor? % :width :height)))

we get the result that

(spec/valid? ::my {:width nil})
=> true
(spec/valid? ::my {:width nil :height 5})
=> false
Ectoparasite answered 12/4, 2017 at 15:42 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.