|
| 1 | +; Copyright (c) Rich Hickey. All rights reserved. |
| 2 | +; The use and distribution terms for this software are covered by the |
| 3 | +; Eclipse Public License 1.0 (http://opensource.org/licenses/eclipse-1.0.php) |
| 4 | +; which can be found in the file epl-v10.html at the root of this distribution. |
| 5 | +; By using this software in any fashion, you are agreeing to be bound by |
| 6 | +; the terms of this license. |
| 7 | +; You must not remove this notice, or any other, from this software. |
| 8 | + |
| 9 | +(ns cljs.spec |
| 10 | + (:refer-clojure :exclude [+ * and or cat def keys]) |
| 11 | + (:require [cljs.core :as c] |
| 12 | + [clojure.walk :as walk] |
| 13 | + [cljs.spec.gen :as gen] |
| 14 | + [clojure.string :as str])) |
| 15 | + |
| 16 | +(defn- ->sym |
| 17 | + "Returns a symbol from a symbol or var" |
| 18 | + [x] |
| 19 | + (if (var? x) |
| 20 | + (let [^clojure.lang.Var v x] |
| 21 | + (symbol (str (.name (.ns v))) |
| 22 | + (str (.sym v)))) |
| 23 | + x)) |
| 24 | + |
| 25 | +(defn- unfn [expr] |
| 26 | + (if (c/and (seq? expr) |
| 27 | + (symbol? (first expr)) |
| 28 | + (= "fn*" (name (first expr)))) |
| 29 | + (let [[[s] & form] (rest expr)] |
| 30 | + (conj (walk/postwalk-replace {s '%} form) '[%] 'fn)) |
| 31 | + expr)) |
| 32 | + |
| 33 | +(defn- res [form] |
| 34 | + (cond |
| 35 | + (keyword? form) form |
| 36 | + (symbol? form) (c/or (-> form resolve ->sym) form) |
| 37 | + (sequential? form) (walk/postwalk #(if (symbol? %) (res %) %) (unfn form)) |
| 38 | + :else form)) |
| 39 | + |
| 40 | +(defmacro def |
| 41 | + "Given a namespace-qualified keyword or symbol k, and a spec, spec-name, predicate or regex-op |
| 42 | + makes an entry in the registry mapping k to the spec" |
| 43 | + [k spec-form] |
| 44 | + `(cljs.spec/def-impl ~k '~(res spec-form) ~spec-form)) |
| 45 | + |
| 46 | +(defmacro spec |
| 47 | + "Takes a single predicate form, e.g. can be the name of a predicate, |
| 48 | + like even?, or a fn literal like #(< % 42). Note that it is not |
| 49 | + generally necessary to wrap predicates in spec when using the rest |
| 50 | + of the spec macros, only to attach a unique generator |
| 51 | +
|
| 52 | + Can also be passed the result of one of the regex ops - |
| 53 | + cat, alt, *, +, ?, in which case it will return a regex-conforming |
| 54 | + spec, useful when nesting an independent regex. |
| 55 | + --- |
| 56 | +
|
| 57 | + Optionally takes :gen generator-fn, which must be a fn of no args that |
| 58 | + returns a test.check generator. |
| 59 | +
|
| 60 | + Returns a spec." |
| 61 | + [form & {:keys [gen]}] |
| 62 | + `(cljs.spec/spec-impl '~(res form) ~form ~gen nil)) |
| 63 | + |
| 64 | +(defmacro multi-spec |
| 65 | + "Takes the name of a spec/predicate-returning multimethod and a |
| 66 | + tag-restoring keyword or fn (retag). Returns a spec that when |
| 67 | + conforming or explaining data will pass it to the multimethod to get |
| 68 | + an appropriate spec. You can e.g. use multi-spec to dynamically and |
| 69 | + extensibly associate specs with 'tagged' data (i.e. data where one |
| 70 | + of the fields indicates the shape of the rest of the structure). |
| 71 | +
|
| 72 | + (defmulti mspec :tag) |
| 73 | +
|
| 74 | + The methods should ignore their argument and return a predicate/spec: |
| 75 | + (defmethod mspec :int [_] (s/keys :req-un [::tag ::i])) |
| 76 | +
|
| 77 | + retag is used during generation to retag generated values with |
| 78 | + matching tags. retag can either be a keyword, at which key the |
| 79 | + dispatch-tag will be assoc'ed, or a fn of generated value and |
| 80 | + dispatch-tag that should return an appropriately retagged value. |
| 81 | +
|
| 82 | + Note that because the tags themselves comprise an open set, |
| 83 | + the tag key spec cannot enumerate the values, but can e.g. |
| 84 | + test for keyword?. |
| 85 | +
|
| 86 | + Note also that the dispatch values of the multimethod will be |
| 87 | + included in the path, i.e. in reporting and gen overrides, even |
| 88 | + though those values are not evident in the spec. |
| 89 | +" |
| 90 | + [mm retag] |
| 91 | + `(cljs.spec/multi-spec-impl '~(res mm) (var ~mm) ~retag)) |
| 92 | + |
| 93 | +(defmacro keys |
| 94 | + "Creates and returns a map validating spec. :req and :opt are both |
| 95 | + vectors of namespaced-qualified keywords. The validator will ensure |
| 96 | + the :req keys are present. The :opt keys serve as documentation and |
| 97 | + may be used by the generator. |
| 98 | +
|
| 99 | + The :req key vector supports 'and' and 'or' for key groups: |
| 100 | +
|
| 101 | + (s/keys :req [::x ::y (or ::secret (and ::user ::pwd))] :opt [::z]) |
| 102 | +
|
| 103 | + There are also -un versions of :req and :opt. These allow |
| 104 | + you to connect unqualified keys to specs. In each case, fully |
| 105 | + qualfied keywords are passed, which name the specs, but unqualified |
| 106 | + keys (with the same name component) are expected and checked at |
| 107 | + conform-time, and generated during gen: |
| 108 | +
|
| 109 | + (s/keys :req-un [:my.ns/x :my.ns/y]) |
| 110 | +
|
| 111 | + The above says keys :x and :y are required, and will be validated |
| 112 | + and generated by specs (if they exist) named :my.ns/x :my.ns/y |
| 113 | + respectively. |
| 114 | +
|
| 115 | + In addition, the values of *all* namespace-qualified keys will be validated |
| 116 | + (and possibly destructured) by any registered specs. Note: there is |
| 117 | + no support for inline value specification, by design. |
| 118 | +
|
| 119 | + Optionally takes :gen generator-fn, which must be a fn of no args that |
| 120 | + returns a test.check generator." |
| 121 | + [& {:keys [req req-un opt opt-un gen]}] |
| 122 | + (let [unk #(-> % name keyword) |
| 123 | + req-keys (filterv keyword? (flatten req)) |
| 124 | + req-un-specs (filterv keyword? (flatten req-un)) |
| 125 | + _ (assert (every? #(c/and (keyword? %) (namespace %)) (concat req-keys req-un-specs opt opt-un)) |
| 126 | + "all keys must be namespace-qualified keywords") |
| 127 | + req-specs (into req-keys req-un-specs) |
| 128 | + req-keys (into req-keys (map unk req-un-specs)) |
| 129 | + opt-keys (into (vec opt) (map unk opt-un)) |
| 130 | + opt-specs (into (vec opt) opt-un) |
| 131 | + parse-req (fn [rk f] |
| 132 | + (map (fn [x] |
| 133 | + (if (keyword? x) |
| 134 | + `#(contains? % ~(f x)) |
| 135 | + (let [gx (gensym)] |
| 136 | + `(fn* [~gx] |
| 137 | + ~(walk/postwalk |
| 138 | + (fn [y] (if (keyword? y) `(contains? ~gx ~(f y)) y)) |
| 139 | + x))))) |
| 140 | + rk)) |
| 141 | + pred-exprs [`map?] |
| 142 | + pred-exprs (into pred-exprs (parse-req req identity)) |
| 143 | + pred-exprs (into pred-exprs (parse-req req-un unk)) |
| 144 | + pred-forms (walk/postwalk res pred-exprs)] |
| 145 | + ;; `(map-spec-impl ~req-keys '~req ~opt '~pred-forms ~pred-exprs ~gen) |
| 146 | + `(cljs.spec/map-spec-impl {:req '~req :opt '~opt :req-un '~req-un :opt-un '~opt-un |
| 147 | + :req-keys '~req-keys :req-specs '~req-specs |
| 148 | + :opt-keys '~opt-keys :opt-specs '~opt-specs |
| 149 | + :pred-forms '~pred-forms |
| 150 | + :pred-exprs ~pred-exprs |
| 151 | + :gfn ~gen}))) |
| 152 | + |
| 153 | +(defmacro or |
| 154 | + "Takes key+pred pairs, e.g. |
| 155 | +
|
| 156 | + (s/or :even even? :small #(< % 42)) |
| 157 | +
|
| 158 | + Returns a destructuring spec that |
| 159 | + returns a vector containing the key of the first matching pred and the |
| 160 | + corresponding value." |
| 161 | + [& key-pred-forms] |
| 162 | + (let [pairs (partition 2 key-pred-forms) |
| 163 | + keys (mapv first pairs) |
| 164 | + pred-forms (mapv second pairs) |
| 165 | + pf (mapv res pred-forms)] |
| 166 | + (assert (c/and (even? (count key-pred-forms)) (every? keyword? keys)) "spec/or expects k1 p1 k2 p2..., where ks are keywords") |
| 167 | + `(cljs.spec/or-spec-impl ~keys '~pf ~pred-forms nil))) |
| 168 | + |
| 169 | +(defmacro and |
| 170 | + "Takes predicate/spec-forms, e.g. |
| 171 | +
|
| 172 | + (s/and even? #(< % 42)) |
| 173 | +
|
| 174 | + Returns a spec that returns the conformed value. Successive |
| 175 | + conformed values propagate through rest of predicates." |
| 176 | + [& pred-forms] |
| 177 | + `(cljs.spec/and-spec-impl '~(mapv res pred-forms) ~(vec pred-forms) nil)) |
| 178 | + |
| 179 | +(defmacro * |
| 180 | + "Returns a regex op that matches zero or more values matching |
| 181 | + pred. Produces a vector of matches iff there is at least one match" |
| 182 | + [pred-form] |
| 183 | + `(cljs.spec/rep-impl '~(res pred-form) ~pred-form)) |
| 184 | + |
| 185 | +(defmacro + |
| 186 | + "Returns a regex op that matches one or more values matching |
| 187 | + pred. Produces a vector of matches" |
| 188 | + [pred-form] |
| 189 | + `(cljs.spec/rep+impl '~(res pred-form) ~pred-form)) |
| 190 | + |
| 191 | +(defmacro ? |
| 192 | + "Returns a regex op that matches zero or one value matching |
| 193 | + pred. Produces a single value (not a collection) if matched." |
| 194 | + [pred-form] |
| 195 | + `(cljs.spec/maybe-impl ~pred-form '~pred-form)) |
| 196 | + |
| 197 | +(defmacro alt |
| 198 | + "Takes key+pred pairs, e.g. |
| 199 | +
|
| 200 | + (s/alt :even even? :small #(< % 42)) |
| 201 | +
|
| 202 | + Returns a regex op that returns a vector containing the key of the |
| 203 | + first matching pred and the corresponding value." |
| 204 | + [& key-pred-forms] |
| 205 | + (let [pairs (partition 2 key-pred-forms) |
| 206 | + keys (mapv first pairs) |
| 207 | + pred-forms (mapv second pairs) |
| 208 | + pf (mapv res pred-forms)] |
| 209 | + (assert (c/and (even? (count key-pred-forms)) (every? keyword? keys)) "alt expects k1 p1 k2 p2..., where ks are keywords") |
| 210 | + `(cljs.spec/alt-impl ~keys ~pred-forms '~pf))) |
| 211 | + |
| 212 | +(defmacro cat |
| 213 | + "Takes key+pred pairs, e.g. |
| 214 | +
|
| 215 | + (s/cat :e even? :o odd?) |
| 216 | +
|
| 217 | + Returns a regex op that matches (all) values in sequence, returning a map |
| 218 | + containing the keys of each pred and the corresponding value." |
| 219 | + [& key-pred-forms] |
| 220 | + (let [pairs (partition 2 key-pred-forms) |
| 221 | + keys (mapv first pairs) |
| 222 | + pred-forms (mapv second pairs) |
| 223 | + pf (mapv res pred-forms)] |
| 224 | + ;;(prn key-pred-forms) |
| 225 | + (assert (c/and (even? (count key-pred-forms)) (every? keyword? keys)) "cat expects k1 p1 k2 p2..., where ks are keywords") |
| 226 | + `(cljs.spec/cat-impl ~keys ~pred-forms '~pf))) |
| 227 | + |
| 228 | +(defmacro & |
| 229 | + "takes a regex op re, and predicates. Returns a regex-op that consumes |
| 230 | + input as per re but subjects the resulting value to the |
| 231 | + conjunction of the predicates, and any conforming they might perform." |
| 232 | + [re & preds] |
| 233 | + (let [pv (vec preds)] |
| 234 | + `(cljs.spec/amp-impl ~re ~pv '~pv))) |
| 235 | + |
| 236 | +(defmacro conformer |
| 237 | + "takes a predicate function with the semantics of conform i.e. it should return either a |
| 238 | + (possibly converted) value or :clojure.spec/invalid, and returns a |
| 239 | + spec that uses it as a predicate/conformer" |
| 240 | + [f] |
| 241 | + `(cljs.spec/spec-impl '~f ~f nil true)) |
| 242 | + |
| 243 | +(defmacro fspec |
| 244 | + "takes :args :ret and (optional) :fn kwargs whose values are preds |
| 245 | + and returns a spec whose conform/explain take a fn and validates it |
| 246 | + using generative testing. The conformed value is always the fn itself. |
| 247 | +
|
| 248 | + Optionally takes :gen generator-fn, which must be a fn of no args |
| 249 | + that returns a test.check generator." |
| 250 | + [& {:keys [args ret fn gen]}] |
| 251 | + `(cljs.spec/fspec-impl ~args '~(res args) ~ret '~(res ret) ~fn '~(res fn) ~gen)) |
| 252 | + |
| 253 | +(defmacro tuple |
| 254 | + "takes one or more preds and returns a spec for a tuple, a vector |
| 255 | + where each element conforms to the corresponding pred. Each element |
| 256 | + will be referred to in paths using its ordinal." |
| 257 | + [& preds] |
| 258 | + (assert (not (empty? preds))) |
| 259 | + `(cljs.spec/tuple-impl '~(mapv res preds) ~(vec preds))) |
| 260 | + |
| 261 | +(defn- ns-qualify |
| 262 | + "Qualify symbol s by resolving it or using the current *ns*." |
| 263 | + [s] |
| 264 | + (if-let [resolved (resolve s)] |
| 265 | + (->sym resolved) |
| 266 | + (if (namespace s) |
| 267 | + s |
| 268 | + (symbol (str (.name *ns*)) (str s))))) |
| 269 | + |
| 270 | +(defn- fn-spec-sym |
| 271 | + [sym role] |
| 272 | + (symbol (str (ns-qualify sym) "$" (name role)))) |
| 273 | + |
| 274 | +(defmacro fdef |
| 275 | + "Takes a symbol naming a function, and one or more of the following: |
| 276 | +
|
| 277 | + :args A regex spec for the function arguments as they were a list to be |
| 278 | + passed to apply - in this way, a single spec can handle functions with |
| 279 | + multiple arities |
| 280 | + :ret A spec for the function's return value |
| 281 | + :fn A spec of the relationship between args and ret - the |
| 282 | + value passed is {:args conformed-args :ret conformed-ret} and is |
| 283 | + expected to contain predicates that relate those values |
| 284 | +
|
| 285 | + Qualifies fn-sym with resolve, or using *ns* if no resolution found. |
| 286 | + Registers specs in the global registry, where they can be retrieved |
| 287 | + by calling fn-specs. |
| 288 | +
|
| 289 | + Once registered, function specs are included in doc, checked by |
| 290 | + instrument, tested by the runner clojure.spec.test/run-tests, and (if |
| 291 | + a macro) used to explain errors during macroexpansion. |
| 292 | +
|
| 293 | + Note that :fn specs require the presence of :args and :ret specs to |
| 294 | + conform values, and so :fn specs will be ignored if :args or :ret |
| 295 | + are missing. |
| 296 | +
|
| 297 | + Returns the qualified fn-sym. |
| 298 | +
|
| 299 | + For example, to register function specs for the symbol function: |
| 300 | +
|
| 301 | + (s/fdef clojure.core/symbol |
| 302 | + :args (s/alt :separate (s/cat :ns string? :n string?) |
| 303 | + :str string? |
| 304 | + :sym symbol?) |
| 305 | + :ret symbol?)" |
| 306 | + [fn-sym & {:keys [args ret fn] :as m}] |
| 307 | + (let [qn (ns-qualify fn-sym)] |
| 308 | + `(do ~@(reduce |
| 309 | + (c/fn [defns role] |
| 310 | + (if (contains? m role) |
| 311 | + (let [s (fn-spec-sym qn (name role))] |
| 312 | + (conj defns `(cljs.spec/def '~s ~(get m role)))) |
| 313 | + defns)) |
| 314 | + [] [:args :ret :fn]) |
| 315 | + '~qn))) |
| 316 | + |
| 317 | +(defmacro with-instrument-disabled |
| 318 | + "Disables instrument's checking of calls, within a scope." |
| 319 | + [& body] |
| 320 | + `(binding [*instrument-enabled* nil] |
| 321 | + ~@body)) |
| 322 | + |
| 323 | +(defmacro keys* |
| 324 | + "takes the same arguments as spec/keys and returns a regex op that matches sequences of key/values, |
| 325 | + converts them into a map, and conforms that map with a corresponding |
| 326 | + spec/keys call: |
| 327 | +
|
| 328 | + user=> (s/conform (s/keys :req-un [::a ::c]) {:a 1 :c 2}) |
| 329 | + {:a 1, :c 2} |
| 330 | + user=> (s/conform (s/keys* :req-un [::a ::c]) [:a 1 :c 2]) |
| 331 | + {:a 1, :c 2} |
| 332 | +
|
| 333 | + the resulting regex op can be composed into a larger regex: |
| 334 | +
|
| 335 | + user=> (s/conform (s/cat :i1 integer? :m (s/keys* :req-un [::a ::c]) :i2 integer?) [42 :a 1 :c 2 :d 4 99]) |
| 336 | + {:i1 42, :m {:a 1, :c 2, :d 4}, :i2 99}" |
| 337 | + [& kspecs] |
| 338 | + `(& (* (cat ::k keyword? ::v ::any)) ::kvs->map (keys ~@kspecs))) |
| 339 | + |
| 340 | +(defmacro nilable |
| 341 | + "returns a spec that accepts nil and values satisfiying pred" |
| 342 | + [pred] |
| 343 | + `(and (or ::nil nil? ::pred ~pred) (conformer second))) |
| 344 | + |
| 345 | +(defmacro coll-of |
| 346 | + "Returns a spec for a collection of items satisfying pred. The generator will fill an empty init-coll." |
| 347 | + [pred init-coll] |
| 348 | + `(spec (cljs.spec/coll-checker ~pred) :gen (cljs.spec/coll-gen ~pred ~init-coll))) |
| 349 | + |
| 350 | +(defmacro map-of |
| 351 | + "Returns a spec for a map whose keys satisfy kpred and vals satisfy vpred." |
| 352 | + [kpred vpred] |
| 353 | + `(and (coll-of (tuple ~kpred ~vpred) {}) map?)) |
0 commit comments