Skip to content

Instantly share code, notes, and snippets.

Last active Mar 4, 2019
What would you like to do?
(ns spec-plus.core
(:require #?@(:clj [[clojure.spec.alpha :as s]]
:cljs [[cljs.spec.alpha :as s]])
[clojure.set :as set])
#?(:cljs (:require-macros [spec-plus.core])))
(defn -cljs-env?
"Take the &env from a macro, and tell whether we are expanding into cljs.
(boolean (:ns env))))
(defmacro -keys
"Explanation of this pattern:"
[& body]
(if (-cljs-env? &env)
`(cljs.spec.alpha/keys ~@body)
`(clojure.spec.alpha/keys ~@body))))
(defmacro -registry
(if (-cljs-env? &env)
(defmacro -and
[& body]
(if (-cljs-env? &env)
`(cljs.spec.alpha/and ~@body)
`(clojure.spec.alpha/and ~@body))))
(defn -unqualify
(keyword (name k)))
(defn -with-explain
"Will use (update-problem [x problem]) to update all the problems produced by explain-data."
[spec update-problem]
{:pre [(s/spec? spec) (ifn? update-problem)]}
(reify s/Spec
[_ path via in x]
(let [data (s/explain* spec path via in x)]
(map #(update-problem x %) data)))
; Do not modify other methods
(conform* [_ x] (s/conform* spec x))
(unform* [_ y] (s/unform* spec y))
(gen* [_ overrides path rmap] (s/gen* spec overrides path rmap))
(with-gen* [_ gfn] (s/with-gen* spec gfn))
(describe* [_] (s/describe* spec))))
(defn -limit-keys
"Constructs a spec which fails if there are keys not from the specified list."
(letfn [(no-disallowed-keys?
(set/subset? (set (keys m)) allowed-keys-set))]
(-with-explain (s/spec no-disallowed-keys?)
(fn [m problem]
(assoc problem :disallowed-keys (set/difference (set (keys m)) allowed-keys-set))))))
(defmacro speced-keys
"Similar to s/keys, but asserts during execution that all keys (passed as arguments to this macro)
have specs already registered.
Map can be closed (true by default) to disallow unknown keys.
Does not support recursive spec definitions, i.e. this will fail: (s/def ::m (speced-keys :opt [::m])).
The workaround is to register the keyword first: (s/def ::m any?) (s/def ::m ...).
Also see discussion at!topic/clojure/i8Rz-AnCoa8."
[& {:keys [closed? req req-un opt opt-un]
:or {closed? true}
:as args}]
(let [args (select-keys args [:closed? :req :req-un :opt :opt-un])
keys-args (dissoc args :closed?)
map-keys (set (apply concat (vals keys-args)))
qualified-map-keys (set (concat req opt))
unqualified-map-keys (set (map -unqualify (concat req-un opt-un)))
allowed-keys (set/union qualified-map-keys unqualified-map-keys)]
`(let [speced-keys# (set (keys (-registry)))
unspeced-keys# (set/difference ~map-keys speced-keys#)
keys-spec# (-keys ~@(apply concat keys-args))]
(when (seq unspeced-keys#)
(throw (ex-info (str "these map keys have no specs registered: " (pr-str unspeced-keys#)) {})))
(if ~closed?
(-limit-keys ~allowed-keys))
; In the future it can also contain :closed? field
{::speced-keys {:keys-spec keys-spec#
:allowed-keys ~allowed-keys}})))))
(defn speced-keys-data
"Returns speced-keys data from the specified spec (identifier or instance) or throws."
(if-some [result (::speced-keys (meta (if (ident? spec)
(s/get-spec spec)
(throw (ex-info (str (pr-str spec) " is not a speced-keys spec: " (pr-str spec)) {}))))
(defn merge-keys
"Constructs a single closed speced-keys spec from other speced-keys specs.
(In the future it can be possible to explicitly (sp/open ...)/(sp/close ...) the specified speced-keys spec.)"
[& specs]
(let [data (map speced-keys-data specs)
keys-specs (map :keys-spec data)
keys-spec (s/merge-spec-impl (mapv s/form keys-specs) keys-specs nil)
allowed-keys (apply set/union (map :allowed-keys data))]
(-limit-keys allowed-keys))
{::speced-keys {:keys-spec keys-spec
:allowed-keys allowed-keys}})))
(s/def ::req-field1 any?)
(let [s (sp/speced-keys :req [::req-field1])
value {::req-field1 123
:extra1 100}
; act
actual (s/explain-data s value)]
; assert
(is (= {::s/problems [{:in []
:path []
:pred 'no-disallowed-keys?
:disallowed-keys #{:extra1}
:val value
:via []}]
::s/spec s
::s/value value}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment