Skip to content

Instantly share code, notes, and snippets.

@stuarthalloway
Created October 14, 2017 11:44
Show Gist options
  • Save stuarthalloway/f4c4297d344651c99827769e1c3d34e9 to your computer and use it in GitHub Desktop.
Save stuarthalloway/f4c4297d344651c99827769e1c3d34e9 to your computer and use it in GitHub Desktop.
I think it would be a mistake to introduce temporal coupling to prevent typos.
;; I think it would be a mistake to introduce temporal coupling to prevent typos.
;; The example program below lets you identify "missing" keys specs at
;; the time and place of your choosing, and then handle them as you
;; deem appropriate, without imposing those decisions on other
;; users of spec.
(require '[clojure.spec.alpha :as s]
'[clojure.set :as set])
(defn keyspec-form?
"Returns true if x is the s/form of a keys spec"
[x]
(and (sequential? x) (= 'clojure.spec.alpha/keys (first x))))
(defn keyspec-form-keys
"Returns set of keys mentioned in s/form of a keys spec"
[x]
(into
#{}
(comp (filter vector?)
cat)
x))
(defn keyspec-keys
"Given an s/registry, returns a map from keys spec names to
the set of qualified keys referenced by the spec."
[registry]
(into
{}
(comp (map (fn [[k v]] [k (s/form v)]))
(filter (fn [[_ v]] (keyspec-form? v)))
(map (fn [[k v]] [k (keyspec-form-keys v)])))
registry))
(defn possible-keyspec-typos
"Given a spec registry, returns a map from keys specs to
keynames missing from the registry. Useful for e.g. finding
typos when you believe you have specified all keys"
[registry]
(let [ksks (keyspec-keys registry)
rks (into #{} (keys registry))]
(into
{}
(comp (map (fn [[n ks]]
[n (set/difference ks rks)]))
(filter (fn [[_ ks]] (seq ks))))
(keyspec-keys registry))))
(comment
(s/def ::foo int?)
(s/def ::bar (s/keys :req-un [::foo ::quux]))
;; this will show you that ::quux is missing
(possible-keyspec-typos (s/registry))
)
@bhb
Copy link

bhb commented Dec 7, 2017

Very cool! Just a head up though for anyone using this code - this won't catch missing/mistyped keys in multi-specs.

  (s/def ::name string?)
  (defmulti msg :msg-type)
  (defmethod msg :greeting [x]
    (s/keys :req-un [::name]))
  (defmethod msg :count [x]
    (s/keys :req-un [::num]))
  (s/def ::msg (s/multi-spec msg :msg-type))

  (possible-keyspec-typos (s/registry)) ; won't detect missing ::num spec

@jpmonettas
Copy link

jpmonettas commented Dec 8, 2017

Something like this could work for catching missing/mistyped kyes in multi-specs:

Added

(defn multi-spec-form?
  "Returns true if x is the s/form of a multi spec"
  [x]
  (and (sequential? x) (= 'clojure.spec.alpha/multi-spec (first x))))



(defn multi-spec-sub-specs
  "Given a multi-spec form, call its multi method methods to retrieve
  its subspecs in the form of [multi-method-key sub-spec-form]."
  [multi-spec-form]
  (let [[_ multi-method-symbol & _] multi-spec-form]
    (->> (resolve multi-method-symbol)
         deref
         methods 
         (map (fn [[spec-k method]]
                [spec-k (s/form (method nil))])))))

and changed keyspec-keys like

(defn keyspec-keys
  "Given an s/registry, returns a map from keys spec names to
the set of qualified keys referenced by the spec."
  [registry]
  (into
   {}
   (comp (mapcat (fn [[k v]] (let [form (s/form v)] ;; <---------- changed this map
                               (if (multi-spec-form? form)
                                 (map (fn [[sk ss]] [[k sk] ss]) (multi-spec-sub-specs form))
                                 [[k form]]))))
         (filter (fn [[_ v]] (keyspec-form? v)))
         (map (fn [[k v]] [k (keyspec-form-keys v)])))
   registry))

and now it returns

(possible-keyspec-typos (s/registry))
;; =>
{:spec-test.core/bar               #{:spec-test.core/quux},
 [:spec-test.core/msg :count] #{:spec-test.core/num}}

@jeroenvandijk
Copy link

I found the following macro variation to enforce some of Plumatic Schema's strictness useful too https://gist.github.com/jeroenvandijk/4fb995a45e49d0e9986d2005ee5840d9

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment