Eroc
Eroc

Reputation: 147

How to create a spec where all keys are optional but at least one of the specified keys should be present?

How am I supposed to create a spec where all keys are optional but at least one of the specified keys should be present?

(s/def ::my-spec (s/and (help-plz??)(s/keys :opt-un [::a ::b]))) 
(s/valid? ::my-spec {} => false
(s/valid? ::my-spec {:a 1}) => true 
(s/valid? ::my-spec {:b 1}) => true 
(s/valid? ::my-spec {:a 1 :b 1}) => true 
(s/valid? ::my-spec {:A1 :B 1}) => true

Upvotes: 3

Views: 1098

Answers (2)

Alex Taggart
Alex Taggart

Reputation: 7825

Per the docs for keys:

The :req key vector supports 'and' and 'or' for key groups:

(s/keys :req [::x ::y (or ::secret (and ::user ::pwd))] :opt [::z])

Your code should be:

(s/def ::my-spec (s/keys :req-un [(or ::a ::b)]))

Upvotes: 7

Taylor Wood
Taylor Wood

Reputation: 16194

With the current spec alpha, in order to use the same key collection for both the keys spec and the at-least-one-exists check, you'll need to use a macro. (The upcoming spec 2 alpha addresses this by exposing more data-driven APIs for creating specs.)

Here's a quick sketch for your particular example:

(defmacro one-or-more-keys [ks]
  (let [keyset (set (map (comp keyword name) ks))]
    `(s/and (s/keys :opt-un ~ks)
            #(some ~keyset (keys %)))))

(s/def ::my-spec (one-or-more-keys [::foo ::bar]))

(s/conform ::my-spec {:bar nil})
=> {:bar nil}
(s/conform ::my-spec {:baz nil})
=> :clojure.spec.alpha/invalid

Alternatively, you could just define the key collection twice, and use a similar predicate with s/and.

Upvotes: 7

Related Questions