Search code examples
clojureclojure.spec

Clojure spec maps


Having two following specs:

(s/def ::x keyword?)
(s/def ::y keyword?)
(s/def ::z keyword?)

(s/def ::a
  (s/keys :req-un [::x
                   ::y]
          :opt-un [::z]))

(s/def ::b
  (s/map-of string? string?))

how do I combine ::a and ::b into ::m so the following data is valid:

(s/valid? ::m
           {:x :foo
            :y :bar
            :z :any})

(s/valid? ::m
          {:x :foo
           :y :bar})

(s/valid? ::m
          {:x :foo
           :y :bar
           :z :baz})

(s/valid? ::m
          {:x :foo
           :y :bar
           :z "baz"})

(s/valid? ::m
          {:x :foo
           :y :bar
           :t "tic"})

additionally, how do I combine ::a and ::b into ::m so the following data is invalid:

(s/valid? ::m
          {"r" "foo"
           "t" "bar"})

(s/valid? ::m
          {:x :foo
           "r" "bar"})

(s/valid? ::m
           {:x :foo
            :y :bar
            :r :any})

Neither of :


(s/def ::m (s/merge ::a ::b))

(s/def ::m (s/or :a ::a :b ::b))

works (as expected), but is there a way to match map entries in priority of the spec order?

The way it should work is the following:

  1. take all the map entries of the value (which is a map)
  2. partition the map entries into two sets. One confirming the ::a spec and the other conforming the ::b spec.
  3. The two sub-maps should conform each the relevant spec as a whole. E.g the first partition should have all the required keys.

Solution

  • You can do this by treating the map not as a map but as a collection of map entries, and then validate the map entries. Handling the "required" keys part has to be done by s/and'ing an additional predicate.

    (s/def ::x keyword?)
    (s/def ::y keyword?)
    (s/def ::z keyword?)
    
    (s/def ::entry (s/or :x (s/tuple #{::x} ::x)
                         :y (s/tuple #{::y} ::y)
                         :z (s/tuple #{::z} ::z)
                         :str (s/tuple string? string?)))
    
    (defn req-keys? [m] (and (contains? m :x) (contains? m :y)))
    
    (s/def ::m (s/and map? (s/coll-of ::entry :into {}) req-keys?))