I haven’t been able to find much online about composing map specs like the following:
(require '[clojure.spec.alpha :as s])
(s/def ::m1 (s/keys :req [::a ::b]))
(s/def ::m2 (s/and ::m1 (s/keys :req [::c ::d])))
This works for validation, but doesn’t create a usable generator for ::m2.
I wrote this macro to allow composing s/keys with s/and:
(defn merge-key-specs [base-spec & {:keys [req req-un opt opt-un gen]
:or {req []
req-un []
opt []
opt-un []}}]
(let [base-keys (reduce (fn [m [k v]] (assoc m k v))
{}
(partition 2 (rest (s/describe base-spec))))
keys (-> {}
(assoc :req (into req (:req base-keys)))
(assoc :req-un (into req-un (:req-un base-keys)))
(assoc :opt (into opt (:opt base-keys)))
(assoc :opt-un (into opt-un (:opt-un base-keys)))
(assoc :gen (or gen (:gen base-spec))))]
keys))
(defmacro and-keys
"Base should name an existing spec, opts are the same as the arguments to s/keys."
[base & opts]
(let [opts (mapcat identity (apply merge-key-specs base opts))]
`(s/keys ~@opts)))
::m2 could then be spec’d like this:
(s/def ::m2 (and-keys ::m1 :req [::c ::d]))
::m2’s generator would now work.
My questions are:
- Is there something built-in for this?
- Is there anything bad about this approach?