0
votes

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.
1
Why would {"r" "foo" "t" "bar"} be invalid? It conforms to the ::b spec?cfrick
It wouldn't be valid as it conforms ONLY to ::b spec. I want to define a spec which would conform to both ::a AND ::b. E.g ::a a requires :x and :y and optionally when :z is present it must be a keyword. Additionally ::m allows for arbitrary keys in the map as long as they are strings and their values are strings as well.Lambder
This sounds not like AND to me, since "all keys must be string" from ::b violates the keywords from ::a. I also see being :t "tic" be valid. So it's either ":x, :y, and maybe :z" OR anything with string keys and values.cfrick
@cfrick thank you very much for your comments. If you are saying, I haven't formulated my problem clearly and correctly you are absolutely right. To be honest I don't know how to express it clearly. I hope that the examples I included do help to understand my need. If you could reformulate my question to be clearer I'd appreciate it.Lambder

1 Answers

1
votes

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?))