Skip to content

Commit 5da67c1

Browse files
committed
first cut at every and every-kv
same as Clojure 03496c03
1 parent a5cb207 commit 5da67c1

File tree

3 files changed

+136
-8
lines changed

3 files changed

+136
-8
lines changed

src/main/cljs/cljs/spec.cljc

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,40 @@
188188
[& pred-forms]
189189
`(cljs.spec/and-spec-impl '~(mapv #(res &env %) pred-forms) ~(vec pred-forms) nil))
190190

191+
(defmacro every
192+
"takes a pred and validates collection elements against that pred.
193+
194+
Note that 'every' does not do exhaustive checking, rather it samples
195+
*coll-check-limit* elements. Nor (as a result) does it do any
196+
conforming of elements. 'explain' will report at most *coll-error-limit*
197+
problems. Thus 'every' should be suitable for potentially large
198+
collections.
199+
200+
Takes several kwargs options that further constrain the collection:
201+
202+
:count - specifies coll has exactly this count (default nil)
203+
:min-count, :max-count - coll has count (<= min count max) (default nil)
204+
:distinct - all the elements are distinct (default nil)
205+
206+
And additional args that control gen
207+
208+
:gen-max - the maximum coll size to generate (default 20)
209+
:gen-into - the default colection to generate into (will be emptied) (default [])
210+
211+
Optionally takes :gen generator-fn, which must be a fn of no args that
212+
returns a test.check generator
213+
"
214+
[pred & {:keys [count max-count min-count distinct gen-max gen-into gen] :as opts}]
215+
`(cljs.spec/every-impl '~pred ~pred ~(dissoc opts :gen) ~gen))
216+
217+
(defmacro every-kv
218+
"like 'every' but takes separate key and val preds and works on associative collections.
219+
220+
Same options as 'every'"
221+
222+
[kpred vpred & opts]
223+
`(every (tuple ~kpred ~vpred) ::kfn (fn [i# v#] (key v#)) :gen-into {} ~@opts))
224+
191225
(defmacro *
192226
"Returns a regex op that matches zero or more values matching
193227
pred. Produces a vector of matches iff there is at least one match"

src/main/cljs/cljs/spec.cljs

Lines changed: 101 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
[cljs.spec.impl.gen :as gen]
1717
[clojure.string :as str]))
1818

19+
(def ^:const MAX_INT 9007199254740991)
20+
1921
(def ^:dynamic *recursion-limit*
2022
"A soft limit on how many times a branching spec (or/alt/*/opt-keys/multi-spec)
2123
can be recursed through during generation. After this a
@@ -27,8 +29,12 @@
2729
21)
2830

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

3339
(def ^:private ^:dynamic *instrument-enabled*
3440
"if false, instrumented fns call straight through"
@@ -180,25 +186,25 @@
180186
;;(prn {:ed ed})
181187
(doseq [[path {:keys [pred val reason via in] :as prob}] (::problems ed)]
182188
(when-not (empty? in)
183-
(print "In:" in ""))
189+
(print "In:" (pr-str in) ""))
184190
(print "val: ")
185191
(pr val)
186192
(print " fails")
187193
(when-not (empty? via)
188-
(print " spec:" (last via)))
194+
(print " spec:" (pr-str (last via))))
189195
(when-not (empty? path)
190-
(print " at:" path))
196+
(print " at:" (pr-str path)))
191197
(print " predicate: ")
192198
(pr pred)
193199
(when reason (print ", " reason))
194200
(doseq [[k v] prob]
195201
(when-not (#{:pred :val :reason :via :in} k)
196-
(print "\n\t" k " ")
202+
(print "\n\t" (pr-str k) " ")
197203
(pr v)))
198204
(newline))
199205
(doseq [[k v] ed]
200206
(when-not (#{::problems} k)
201-
(print k " ")
207+
(print (pr-str k) " ")
202208
(pr v)
203209
(newline)))))
204210
(println "Success!")))
@@ -667,6 +673,94 @@
667673
(with-gen* [_ gfn] (and-spec-impl forms preds gfn))
668674
(describe* [_] `(and ~@forms))))
669675

676+
(defn ^:skip-wiki every-impl
677+
"Do not call this directly, use 'every'"
678+
([form pred opts] (every-impl form pred opts nil))
679+
([form pred {:keys [count max-count min-count distinct gen-max gen-into ::kfn]
680+
:or {gen-max 20, gen-into []}
681+
:as opts}
682+
gfn]
683+
(let [check? #(valid? pred %)
684+
kfn (c/or kfn (fn [i v] i))]
685+
(reify
686+
Spec
687+
(conform* [_ x]
688+
(cond
689+
(c/or (not (seqable? x))
690+
(c/and distinct (not (empty? x)) (not (apply distinct? x)))
691+
(c/and count (not= count (bounded-count (inc count) x)))
692+
(c/and (c/or min-count max-count)
693+
(not (<= (c/or min-count 0)
694+
(bounded-count (if max-count (inc max-count) min-count) x)
695+
(c/or max-count MAX_INT)))))
696+
::invalid
697+
698+
:else
699+
(if (indexed? x)
700+
(let [step (max 1 (long (/ (c/count x) *coll-check-limit*)))]
701+
(loop [i 0]
702+
(if (>= i (c/count x))
703+
x
704+
(if (check? (nth x i))
705+
(recur (c/+ i step))
706+
::invalid))))
707+
(c/or (c/and (every? check? (take *coll-check-limit* x)) x)
708+
::invalid))))
709+
(unform* [_ x] x)
710+
(explain* [_ path via in x]
711+
(cond
712+
(not (seqable? x))
713+
{path {:pred 'seqable? :val x :via via :in in}}
714+
715+
(c/and distinct (not (empty? x)) (not (apply distinct? x)))
716+
{path {:pred 'distinct? :val x :via via :in in}}
717+
718+
(c/and count (not= count (bounded-count count x)))
719+
{path {:pred `(= ~count (c/count %)) :val x :via via :in in}}
720+
721+
(c/and (c/or min-count max-count)
722+
(not (<= (c/or min-count 0)
723+
(bounded-count (if max-count (inc max-count) min-count) x)
724+
(c/or max-count MAX_INT))))
725+
{path {:pred `(<= ~(c/or min-count 0) (c/count %) ~(c/or max-count 'js/Number.MAX_SAFE_INTEGER)) :val x :via via :in in}}
726+
727+
:else
728+
(apply merge
729+
(take *coll-error-limit*
730+
(keep identity
731+
(map (fn [i v]
732+
(let [k (kfn i v)]
733+
(when-not (check? v)
734+
(let [prob (explain-1 form pred (conj path k) via (conj in k) v)]
735+
prob))))
736+
(range) x))))))
737+
(gen* [_ overrides path rmap]
738+
(if gfn
739+
(gfn)
740+
(let [init (empty gen-into)
741+
pgen (gensub pred overrides path rmap form)]
742+
(gen/fmap
743+
#(if (vector? init) % (into init %))
744+
(cond
745+
distinct
746+
(if count
747+
(gen/vector-distinct pgen {:num-elements count :max-tries 100})
748+
(gen/vector-distinct pgen {:min-elements (c/or min-count 0)
749+
:max-elements (c/or max-count (max gen-max (c/* 2 (c/or min-count 0))))
750+
:max-tries 100}))
751+
752+
count
753+
(gen/vector pgen count)
754+
755+
(c/or min-count max-count)
756+
(gen/vector pgen (c/or min-count 0) (c/or max-count (max gen-max (c/* 2 (c/or min-count 0)))))
757+
758+
:else
759+
(gen/vector pgen 0 gen-max))))))
760+
761+
(with-gen* [_ gfn] (every-impl form pred opts gfn))
762+
(describe* [_] `(every ~form ~@(mapcat identity opts)))))))
763+
670764
;;;;;;;;;;;;;;;;;;;;;;; regex ;;;;;;;;;;;;;;;;;;;
671765
;;See:
672766
;; http://matt.might.net/articles/implementation-of-regular-expression-matching-in-scheme-with-derivatives/

src/main/cljs/cljs/spec/impl/gen.cljs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@
6666
; g
6767
; (throw (js/Error. (str "Var " s " is not a generator"))))))
6868

69-
(lazy-combinators hash-map list map not-empty set vector fmap elements
69+
(lazy-combinators hash-map list map not-empty set vector vector-distinct fmap elements
7070
bind choose one-of such-that tuple sample return
7171
large-integer*)
7272

0 commit comments

Comments
 (0)