Composing specs with s/keys and s/and

clojure_spec
#1

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 [email protected])))

::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?
1 Like
#2

Does s/merge do what you want?

#3

Yes it does! Thank you.