Skip to content

Commit ad4561c

Browse files
committed
wip
1 parent f00df07 commit ad4561c

File tree

2 files changed

+1457
-0
lines changed

2 files changed

+1457
-0
lines changed

src/main/clojure/cljs/spec.clj

Lines changed: 353 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,353 @@
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

Comments
 (0)