Skip to content

Commit 03496c0

Browse files
committed
first cut at every and every-kv
1 parent 85a90b2 commit 03496c0

File tree

2 files changed

+134
-8
lines changed

2 files changed

+134
-8
lines changed

src/clj/clojure/spec.clj

+133-7
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@
2727
21)
2828

2929
(def ^:dynamic *coll-check-limit*
30-
"The number of items validated in a collection spec'ed with 'coll'"
31-
100)
30+
"The number of elements validated in a collection spec'ed with 'every'"
31+
101)
32+
33+
(def ^:dynamic *coll-error-limit*
34+
"The number of errors reported by explain in a collection spec'ed with 'every'"
35+
20)
3236

3337
(def ^:private ^:dynamic *instrument-enabled*
3438
"if false, instrumented fns call straight through"
@@ -179,25 +183,25 @@
179183
;;(prn {:ed ed})
180184
(doseq [[path {:keys [pred val reason via in] :as prob}] (::problems ed)]
181185
(when-not (empty? in)
182-
(print "In:" in ""))
186+
(print "In:" (pr-str in) ""))
183187
(print "val: ")
184188
(pr val)
185189
(print " fails")
186190
(when-not (empty? via)
187-
(print " spec:" (last via)))
191+
(print " spec:" (pr-str (last via))))
188192
(when-not (empty? path)
189-
(print " at:" path))
193+
(print " at:" (pr-str path)))
190194
(print " predicate: ")
191195
(pr pred)
192196
(when reason (print ", " reason))
193197
(doseq [[k v] prob]
194198
(when-not (#{:pred :val :reason :via :in} k)
195-
(print "\n\t" k " ")
199+
(print "\n\t" (pr-str k) " ")
196200
(pr v)))
197201
(newline))
198202
(doseq [[k v] ed]
199203
(when-not (#{::problems} k)
200-
(print k " ")
204+
(print (pr-str k) " ")
201205
(pr v)
202206
(newline))))
203207
(println "Success!")))
@@ -432,6 +436,40 @@
432436
[& pred-forms]
433437
`(and-spec-impl '~(mapv res pred-forms) ~(vec pred-forms) nil))
434438

439+
(defmacro every
440+
"takes a pred and validates collection elements against that pred.
441+
442+
Note that 'every' does not do exhaustive checking, rather it samples
443+
*coll-check-limit* elements. Nor (as a result) does it do any
444+
conforming of elements. 'explain' will report at most *coll-error-limit*
445+
problems. Thus 'every' should be suitable for potentially large
446+
collections.
447+
448+
Takes several kwargs options that further constrain the collection:
449+
450+
:count - specifies coll has exactly this count (default nil)
451+
:min-count, :max-count - coll has count (<= min count max) (default nil)
452+
:distinct - all the elements are distinct (default nil)
453+
454+
And additional args that control gen
455+
456+
:gen-max - the maximum coll size to generate (default 20)
457+
:gen-into - the default colection to generate into (will be emptied) (default [])
458+
459+
Optionally takes :gen generator-fn, which must be a fn of no args that
460+
returns a test.check generator
461+
"
462+
[pred & {:keys [count max-count min-count distinct gen-max gen-into gen] :as opts}]
463+
`(every-impl '~pred ~pred ~(dissoc opts :gen) ~gen))
464+
465+
(defmacro every-kv
466+
"like 'every' but takes separate key and val preds and works on associative collections.
467+
468+
Same options as 'every'"
469+
470+
[kpred vpred & opts]
471+
`(every (tuple ~kpred ~vpred) ::kfn (fn [i# v#] (key v#)) :gen-into {} ~@opts))
472+
435473
(defmacro *
436474
"Returns a regex op that matches zero or more values matching
437475
pred. Produces a vector of matches iff there is at least one match"
@@ -1034,6 +1072,94 @@ by ns-syms. Idempotent."
10341072
(with-gen* [_ gfn] (and-spec-impl forms preds gfn))
10351073
(describe* [_] `(and ~@forms))))
10361074

1075+
(defn ^:skip-wiki every-impl
1076+
"Do not call this directly, use 'every'"
1077+
([form pred opts] (every-impl form pred opts nil))
1078+
([form pred {:keys [count max-count min-count distinct gen-max gen-into ::kfn]
1079+
:or {gen-max 20, gen-into []}
1080+
:as opts}
1081+
gfn]
1082+
(let [check? #(valid? pred %)
1083+
kfn (c/or kfn (fn [i v] i))]
1084+
(reify
1085+
Spec
1086+
(conform* [_ x]
1087+
(cond
1088+
(c/or (not (seqable? x))
1089+
(c/and distinct (not (empty? x)) (not (apply distinct? x)))
1090+
(c/and count (not= count (bounded-count (inc count) x)))
1091+
(c/and (c/or min-count max-count)
1092+
(not (<= (c/or min-count 0)
1093+
(bounded-count (if max-count (inc max-count) min-count) x)
1094+
(c/or max-count Integer/MAX_VALUE)))))
1095+
:invalid
1096+
1097+
:else
1098+
(if (indexed? x)
1099+
(let [step (max 1 (long (/ (c/count x) *coll-check-limit*)))]
1100+
(loop [i 0]
1101+
(if (>= i (c/count x))
1102+
x
1103+
(if (check? (nth x i))
1104+
(recur (c/+ i step))
1105+
::invalid))))
1106+
(c/or (c/and (every? check? (take *coll-check-limit* x)) x)
1107+
::invalid))))
1108+
(unform* [_ x] x)
1109+
(explain* [_ path via in x]
1110+
(cond
1111+
(not (seqable? x))
1112+
{path {:pred 'seqable? :val x :via via :in in}}
1113+
1114+
(c/and distinct (not (empty? x)) (not (apply distinct? x)))
1115+
{path {:pred 'distinct? :val x :via via :in in}}
1116+
1117+
(c/and count (not= count (bounded-count count x)))
1118+
{path {:pred `(= ~count (c/count %)) :val x :via via :in in}}
1119+
1120+
(c/and (c/or min-count max-count)
1121+
(not (<= (c/or min-count 0)
1122+
(bounded-count (if max-count (inc max-count) min-count) x)
1123+
(c/or max-count Integer/MAX_VALUE))))
1124+
{path {:pred `(<= ~(c/or min-count 0) (c/count %) ~(c/or max-count 'Integer/MAX_VALUE)) :val x :via via :in in}}
1125+
1126+
:else
1127+
(apply merge
1128+
(take *coll-error-limit*
1129+
(keep identity
1130+
(map (fn [i v]
1131+
(let [k (kfn i v)]
1132+
(when-not (check? v)
1133+
(let [prob (explain-1 form pred (conj path k) via (conj in k) v)]
1134+
prob))))
1135+
(range) x))))))
1136+
(gen* [_ overrides path rmap]
1137+
(if gfn
1138+
(gfn)
1139+
(let [init (empty gen-into)
1140+
pgen (gensub pred overrides path rmap form)]
1141+
(gen/fmap
1142+
#(if (vector? init) % (into init %))
1143+
(cond
1144+
distinct
1145+
(if count
1146+
(gen/vector-distinct pgen {:num-elements count :max-tries 100})
1147+
(gen/vector-distinct pgen {:min-elements (c/or min-count 0)
1148+
:max-elements (c/or max-count (max gen-max (c/* 2 (c/or min-count 0))))
1149+
:max-tries 100}))
1150+
1151+
count
1152+
(gen/vector pgen count)
1153+
1154+
(c/or min-count max-count)
1155+
(gen/vector pgen (c/or min-count 0) (c/or max-count (max gen-max (c/* 2 (c/or min-count 0)))))
1156+
1157+
:else
1158+
(gen/vector pgen 0 gen-max))))))
1159+
1160+
(with-gen* [_ gfn] (every-impl form pred opts gfn))
1161+
(describe* [_] `(every ~form ~@(mapcat identity opts)))))))
1162+
10371163
;;;;;;;;;;;;;;;;;;;;;;; regex ;;;;;;;;;;;;;;;;;;;
10381164
;;See:
10391165
;; http://matt.might.net/articles/implementation-of-regular-expression-matching-in-scheme-with-derivatives/

src/clj/clojure/spec/gen.clj

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@
8989
(fn [s] (c/list 'lazy-combinator s))
9090
syms)))
9191

92-
(lazy-combinators hash-map list map not-empty set vector fmap elements
92+
(lazy-combinators hash-map list map not-empty set vector vector-distinct fmap elements
9393
bind choose fmap one-of such-that tuple sample return
9494
large-integer* double*)
9595

0 commit comments

Comments
 (0)