speculoos.utility

Utility functions to make using and interacting with Speculoos nicer.

  1. Constructive functions create conforming data or specifications, e.g., synthesizing valid data when given a specification.

  2. Destructive” functions return new versions of data and/or specification with non-conforming datums or un-satisfied predicates altered or removed so that validation fully succeeds, e.g., replacing all invalid pairs with nil/nil?.

    All data structures remain immutable. These functions are only “destructive” in the sense that if you forward the new version of data and/or specification, the recipient will have lost some information.

  3. Reporting functions highlight some aspect of a data set or specification, e.g., returning non-predicates within a specification.

  4. Helper functions decrease keypresses, e.g., get fourth element of a sequence.

*ordinal-offset*

dynamic

Governs behavior of =1st, =2nd, through =12th. Bind to -1 (default) for one-based indexing. Bind to 0 for zero-based indexing.

*such-that-max-tries*

dynamic

=10th

See =1st.

=11th

See =1st.

=12th

See =1st.

=1st

(=1st s)
(=2nd s)
(=3rd s)
(=4th s)
(=5th s)
(=6th s)
(=7th s)
(=8th s)
(=9th s)
(=10th s)
(=11th s)
(=12th s)

Convenience functions to access elements of a sequence s. Uses one-based indexing by default.

Examples:

(=1st [:one :two :three]) ;; => :one
(=2nd [:one :two :three]) ;; => :two
(=3rd [:one :two :three]) ;; => :three

(=2nd (list 'foo 'bar 'baz)) ;; => bar

Re-bind *ordinal-offset* for zero-based indexing.

Example:

(binding [*ordinal-offset* 0]
    (=1st [:one :two :three])) ;; => :two

=2nd

See =1st.

=3rd

See =1st.

=4th

See =1st.

=5th

See =1st.

=6th

See =1st.

=7th

See =1st.

=8th

See =1st.

=9th

See =1st.

all-specs-okay

(all-specs-okay spec)

Returns true if all entries in specification spec are a function. Otherwise, returns all-paths entries {:path _ :element _} to non-functions. See non-predicates for limitations.

Examples:

(all-specs-okay [int? keyword? ratio?]) ;; => true
(all-specs-okay [int? keyword? 9.87]) ;; => ({:path [2], :value 9.87})

apathetic

(apathetic data spec)

Return a scalar specification spec for which any specification predicate which returns invalid, is transmorgrified to any?.

Examples:

(apathetic [42 :foo 22/7] [int? symbol? ratio?]) ;; => [int? any? ratio?]
(apathetic {:a 42 :b 'foo} {:a int? :b keyword?}) ;; => {:a int?, :b any?}

basic-collection-spec-from-data

(basic-collection-spec-from-data data)

Given data, a heterogeneous, arbitrarily-nested collection, returns a similar structure that can serve as basic collection specification. Non-terminating sequences are clamped to length 3 and converted to vectors.

Note: Speculoos validates maps with predicates at keys which do not exist in data, so a pseudo-qualified key :speculoos.utility/collection-predicate is created that is applied by validate-collections.

Examples:

(basic-collection-spec-from-data [42 [:foo [22/7]]])
;; => [[[vector?] vector] vector?]

(basic-collection-spec-from-data {:a 42 :b {:c ['foo true]}})
;; => {:b {:c [vector?], :speculoos.utility/collection-predicate map?},
;;     :speculoos.utility/collection-predicate map?}

(basic-collection-spec-from-data [42 #{:foo} (list 22/7) (range)])
;; => [#{set?} (list?) [vector] vector]

clamp-every

(clamp-every coll x)

Given a heterogeneous, arbitrarily-nested collection coll, ‘clamp’ every (possibly) non-terminating sequence at length x. Sequences are converted to vectors. See clamp-in* for particulars on behavior.

Examples:

(clamp-every [42 (range) 99 (repeat :foo) 33] 3)
;; => [42 [0 1 2] 99 [:foo :foo :foo] 33]

(clamp-every {:a (cycle ['foo 'bar 'baz]) :b (iterate inc 100)} 5)
;; => {:a [foo bar baz foo bar], :b [100 101 102 103 104]}

clamp-in*

(clamp-in* coll path x)

Given a heterogeneous, arbitrarily-nested collection coll, ‘clamp’ (possibly) non-terminating sequence at path to length x. Sequences are converted to vectors.

Beware: Behaves like take, so will also possibly shorten vectors, lists, sets, and maps if located at path.

Examples:

(clamp-in* [:foo (range) 'bar] [1] 3)
;; => [:foo [0 1 2] bar]

(clamp-in* {:a [42 'foo {:b (cycle [:x :y :z])}]} [:a 2 :b] 5)
;; => {:a [42 foo {:b [:x :y :z :x :y]}]}

;; does not discriminate; operates on terminating sequences, too
(clamp-in* [:foo 42 [:x :y :z :a :b :c]] [2] 4)
;; => [:foo 42 [:x :y :z :a]]

;; sequences that are shorter than `x` are unchanged
(clamp-in* {:a (take 3 (range))} [:a] 6)
;; => {:a [0 1 2]}

collections-without-predicates

(collections-without-predicates data spec)

Given data a heterogeneous, arbitrarily-nested data structure, returns a set of data’s all-paths elements for collections which lack at least one corresponding predicate in collection specification spec. See also scalars-without-predicates and scalars-with-predicates.

Examples:

;; all collections have a corresponding collection predicate; returns an empty set
(collections-without-predicates [11 [22] 33] [vector? [vector?]])
;; => #{}

;; missing predicate(s)
(collections-without-predicates [11 [22] 33] [vector? []])
;; => #{{:path [1], :value [22]}}

(collections-without-predicates {:a [11 {:b (list 22 33)}]} {:a [vector? {:is-a-map? map?}]})
;; => #{{:path [:a 1 :b], :value (22 33)}
;;      {:path [], :value {:a [11 {:b (22 33)}]}}}

(collections-without-predicates [11 [22] 33 (cycle [44 55 66])] [vector? [vector?]])
;; => #{{:path [3], :value []}}

data-from-spec

(data-from-spec spec)(data-from-spec spec opt)

Given heterogeneous, arbitrarily-nested scalar specification spec, create a sample data structure whose values validate to that specification. Works for singular, clojure.core predicates, e.g., int?, string?, or for #(and (int? %) (< 5 %)) with generators at metadata key :speculoos/predicate->generator. Unknown predicates yield nil. Defaults to canonical values, i.e., 'abc' for string?.

Optional trailing arg :random uses clojure.test.check generators when available. Trailing arg :canonical (default) uses lookup table to supply aesthetic values, i.e., 42 for int?.

Sets are assumed to be a predicate. :canonical uses whatever clojure.core/first returns (consider a sorted set with the first element an exemplar); :random yields a random selection from the set.

Bare regular expressions yield random strings that match.

Examples:

;; default canonical datums
(data-from-spec [int? keyword? ratio?]) ;; => [42 :kw 22/7]
(data-from-spec {:a symbol? :b decimal?}) ;; => {:a speculoos/canonical-symbol, :b 1M}

;; optional random values
(data-from-spec [int? [keyword? [ratio?]]] :random) ;; => [-715 [:! [15/2]]]
(data-from-spec {:a symbol? :b {:c boolean? :d pos-int?}} :random) ;; => {:a x2_qV/x, :b {:c true, :d 26}}

;; set as a predicate, defaults to (first #{...})
(data-from-spec [int? #{:red :green :blue} ratio?]) ;; => [42 :green 22/7]

;; set as a predicate, optional random selection
(data-from-spec [int? #{:red :green :blue} ratio?] :random) ;; => [-208 :red 7/2]

;; regular expression as a predicate
(data-from-spec [#"Q\d{3}Y\d{1,2}" #"foo[2468]"])
;; => ["Q690Y61" "foo6"]

If the metadata of a non-set, non-regex predicate contains either key :speculoos/predicate->generator or key :speculoos/canonical-sample, the associated value is presumed to be a competent generator or canonical sample, respectively, and will be preferred. See validate-predicate->generator.

Note: Presence of either metadata key implies that a default generator will not be found in the lookup table, and therefore requires a customized generator for invoking with the :random option. But it also implies that a canonical value will also not be located, and therefore requires a custom canonical value, which is referenced when explicitly invoked with :canonical, but also the default, no-option invocation.

;; as is, Speculoos cannot generate a sample datum from this predicate
(def opaque-predicate (fn [x] (and (int? x) (pos? x) (< x 99))))

;; Speculoos can however retrieve a generator at metadata key :speculoos/predicate->generator
(data-from-spec {:a string? :b (with-meta opaque-predicate {:speculoos/predicate->generator #(rand-int 100)})} :random)
;; => {:a "Q56BPs2ownU08ExMOy3yVD37M6Q", :b 15}

;; predicates not found in the lookup table must explicitly provide a canonical value when invoking without `opt`
(data-from-spec [ratio? (with-meta int? {:speculoos/canonical-sample 987654321})])
;; => [22/7 987654321]

See defpred for a helper to set up a predicate with a generator.

(defpred f4 int? #(rand-int 99)) ;; optional canonical value not supplied
(defpred f5 number? #(rand 999) 1.234)

(data-from-spec {:a f4 :b [f5]} :random)
;; => {:a 31, :b [147.28249222486664]}

(data-from-spec {:a f4 :b [f5]} :canonical)
;; => {:a :f4-canonical-sample, :b [1.234]}

defpred

macro

(defpred predname f)(defpred predname f generator)(defpred predname f generator canonical)

define a predicate by binding symbol predname to function f while associating a random sample generator.

  1. With only a name predname and function f, defpred will attempt to create a random sample generator as outlined by inspect-fn.
  2. An explicitly supplied generator will be preferred.
  3. If generator is explicitly provided, a canonical value may also be supplied. If the optional canonical value is not supplied, a keyword is automatically generated using the symbol predname appended with -canonical-sample.

Useful for defining predicates consumed by data-from-spec, exercise, and friends.

*such-that-max-tries* is bound to an integer (default 50) that governs the number of attempts the random sample generator will make to create a valid sample.

Note: Can not use bare predicates such as int?; they must be ‘wrapped’, like #(int? %). See examples.

Examples:

(macroexpand-1 '(defpred foo (fn [i] (int? i)) (fn [] (rand-int 99)) 42))
;; => (def foo (clojure.core/with-meta (fn [i] (int? i)) #:speculoos{:canonical-sample 42, :predicate->generator (fn [] (rand-int 99))}))

(defpred f1 #(<= % 99) #(rand-int 99) 42)
(defpred f2 #(<= % 5)  #(- (rand-int 99))) ;; no canonical value supplied
(defpred f3 #(> % 999) #(rand 999) 1.23)

(data-from-spec [f1 f2 f3] :random)
;; => [86 -77 971.3462532541377]

(data-from-spec [f1 f2 f3] :canonical)
;; => [42 :f2-canonical-sample 1.23]

;; using (and...) to modify a core predicate
(defpred even-int? #(and (int? %) (even? %) (< 3 %)))
(data-from-spec [even-int? even-int? even-int? even-int?] :random)
;; => [14 24 16 30]

;; using (or...) to generate alternatives
(defpred int-or-kw? #(or (int? %) (keyword? %)))
(data-from-spec [int-or-kw? int-or-kw? int-or-kw? int-or-kw?] :random)
;; => [:J0+ -25 22 :o_6W.l:?]

;; using both (and...) and (or...)
(defpred odd-int-or-short-kw? (fn [x] (or (and (int? x)
                                               (odd? x))
                                          (and (keyword? x)
                                               (>= 3 (count (str x)))))))

(data-from-spec [odd-int-or-short-kw?
                 odd-int-or-short-kw?
                 odd-int-or-short-kw?
                 odd-int-or-short-kw?] :random)
;; => [:Y9.c :i. -3 :xxk]

exercise

(exercise spec)(exercise spec n)

Generates a number n (default 10) of values compatible with scalar specification spec and maps valid-scalars? over them, returning a sequence of [val valid?] tuples.

If n is :canonical, only one data set is produced, consisting of the predicates’ canonical values.

See data-from-spec for details on predicates.

Examples, passing optional count n 3 for brevity:

(exercise [int? [keyword? ratio?] char?] 3)
;; => ([[-282 [:_1z9L -17/21] \G] true]
;;     [[ 469 [:c*2y   -7/11] \9 true]
;;     [[-293 [:*C     -3/7 ] \i] true])

(exercise {:a symbol? :b [pos-int? decimal? char?]} 3)
;; => ([{:a U8D_/gs+H,   :b [17 1M \Z]} true]
;;     [{:a b-187+/Xv,   :b [27 1M \3]} true]
;;     [{:a ?l4zv/Y.Ro!, :b [22 1M \G]} true])

Example with canonical values:

(exercise [int? char? ratio? string? double?] :canonical)
;; => ([[42 \c 22/7 "abc" 1.0E32] true])

in?

(in? coll item)

Returns true if item is found somewhere in collection coll. This utility function is a replacement for what the name clojure.core/contains? suggests it might do. Works on all collection types, but map elements are checked as MapEntry key-value pairs. Pass (vals m) if you want to only check values. Properly handles nil and false membership.

Examples:

;; `contains?` tests for existence of a sequence's index, not its values
(contains? [:a :b :c] 1) ;; => true

;; `in?` tests for existence of a sequence's values
(in? [:a :b :c] 1) ;; => false

(in? [:a :b :c] :b) ;; true
(in? (list 'foo 'bar 'baz) 'foo) ;; => true
(in? #{:red :green :blue} :green) ;; => true
(in? (range) 3) ;; => true

;; elements of a map are tested as MapEntries
(in? {:a 1 :b 2 :c 3} 3) ;; => false

;; passing just the vals of a map
(in? (vals {:a 1 :b 2 :c 3}) 3) ;; => true

;; testing existence of a MapEntry
(in? {:a 1 :b 2 :c 3} [:c 3]) ;; => true

;; nil presence
(in? [42 nil 22/7] nil) ;; => true

;; false presence
(in? (vals {:a 42 :b false}) false) ;; => true

inspect-fn

(inspect-fn f)

Inspect expression f to attempt to create a random sample generator. If unable to do so, returns nil. f must be of the form (fn [...] ...) or #(...). Returns a vector whose first element is the symbolic representation of the generator and whose second element is an invocable generator object.

This implementation is a proof-of-concept that only descends to a maximum depth of two levels. It does not generically handle arbitrary nesting depths nor all possible Clojure data types

The predicate body must fulfill these properties:

  1. The first symbol must be and, or, or a basic predicate for a Clojure built-in scalar, such as int? that is registered at speculoos.utility/predicate->generator.
  2. The first clause after and or or must contain a clojure.core predicate for a scalar, such as int?.
  3. Subsequent clauses of and will be injected into a clojure.test.check.generators/such-that filter.
  4. Direct descendants of a top-level or will produce n separate random sample generators, each with 1/n probability for any one invocation.

Examples:

(inspect-fn '(fn [i] (int? i)))
(inspect-fn '(fn [i] (and (int? i) (even? i))))

(inspect-fn '#(int? %))
(inspect-fn '#(and (int? %) (even? %)))

(inspect-fn '(fn [x] (or (int? x) (ratio? x))))

(inspect-fn '#(or (and (int? %) (odd? %))
                  (and (string? %) (<= 3 (count %)))))

lazy-seq?

(lazy-seq? x)

Returns true if x is a lazy sequence.

non-predicates

(non-predicates spec)

Returns all-paths entries {:path _ :value _} for all elements in specification spec that are not functions.

This function name is possibly mis-leading: It tests only fn?, not if the function is a competent predicate. Sets are flagged as ‘non-predicates’.

Examples:

(non-predicates [int? 42 decimal?]) ;; => ({:path [1], :value 42})

(non-predicates {:a int? :b 'foo}) ;; => ({:path [:b], :value foo})

Sets may serve as a membership predicate for a scalar specification, but are flagged as ‘non-predicates’.

Demonstration:

;; `validate-scalars` considers #{:red} as a predicate-like thing, but ...
;; ... `non-predicates` is unable to distinguish without data to accompany spec
(non-predicates [int? #{:red} ratio?]) ;; => ({:path [1 :red], :value :red})

pred-sym->gen-sym

predicate->generator

predicates-without-collections

(predicates-without-collections data spec)

Given data at heterogeneous, arbitrarily-nested data structure and a collection specification spec, returns a set of all-paths elements for predicates in spec which cannnot be paired with a collection element in data. See also collections-without-predicates, scalars-without-predicates, and predicates-without-scalars.

Examples:

;; all predicates are paired
(predicates-without-collections [42] [vector?])
;; => #{}

;; one un-paired predicate
(predicates-without-collections [42] [vector? [map?]])
;; => #{{:path [1 0], :value map?}}

;; one un-paired predicate
(predicates-without-collections {:a 42} {:is-map? map? :b [vector?]})
;; => #{{:path [:b 0], :value vector?}}

;; one un-paired predicate <-- collection validation only applies predicates to non-scalars
(predicates-without-collections {:a 42 :b 99} {:is-map? map? :b [vector?]})
;; => #{{:path [:b 0], :value vector?}}

predicates-without-scalars

(predicates-without-scalars data spec)

Returns only scalar specification spec elements which lack a corresponding element in data.

Examples:

(predicates-without-scalars [42 :foo] [int? keyword? ratio?]) ;; => ({:path [2], :value ratio?})
(predicates-without-scalars {:a 42} {:a int? :b symbol?}) ;; => ({:path [:b], :value symbol?})

scalars-with-predicates

(scalars-with-predicates data spec)

Returns data all-paths scalar elements which have a corresponding predicate in scalar specification spec. See also scalars-without-predicates and collections-without-predicates.

Examples:

(scalars-with-predicates [42 :foo 22/7] [int?]) ;; => ({:path [0], :value 42})
(scalars-with-predicates {:a 42 :b 'foo} {:a int?}) ;; => ({:path [:a], :value 42})

scalars-without-predicates

(scalars-without-predicates data spec)

Returns a set of data all-paths scalar elements which lack corresponding predicates in scalar specification spec. See also scalars-with-predicates and collections-without-predicates.

Examples:

(scalars-without-predicates [42 :foo 22/7] [int? keyword?]) ;; => #{{:path [2], :value 22/7}}
(scalars-without-predicates {:a 42 :b 'foo} {:a int?}) ;; => #{{:path [:b], :value foo}}

seq-regex

(seq-regex s & pred-regexes)

Returns true if sequence s fully satisfies pred-regex, pairs of predicates and regex-like operators. Predicates partition the sequence, regex operators check the quantity, according to the following.

  • :? zero-or-one
  • :. exactly-one
  • :+ one-or-more
  • :* zero-or-more
  • [m n] between m and n (integers), inclusive
  • i exactly i (integer)

Examples:

(seq-regex [1 2 3 :a :b 'foo] int? :* keyword? :+ symbol? :.) ;; => true

Tests zero-or-more integers, followed by one-or-more keywords, followed by exactly-one symbol.

You must supply predicate and regexes to exhaust the sequence, otherwise returns false. If you don’t care about entities beyond a particular index, use the duplet any? :*.

(seq-regex [1 2 :a :b true false] int? 2 keyword? [1 3] any? :*) ;; => true

Tests exactly two integers, followed by one to three keywords, followed by any number of anything.

Failing example:

(seq-regex [1 2 :a :b] int? 2) ;; => false

Tests exactly two integers; example fails because trailing keywords are not matched.

Any unused pred-regex pairs are ignored.

(seq-regex [1 2 3] int? 3 keyword? 3) ;; => true

If your first regex is zero-or-more :*, then it will match an empty sequence, regardless of any trailing pred-regex pairs.

(seq-regex [] int? :* keyword :+) ;; => true

Do not stack the one-or-more :. regex operator in an attempt to get an integer >1.

(seq-regex [1 2 3] int? :. int? :. int? :.) ;; => false

Instead, use the exactly-integer or the range ops.

(seq-regex [1 2 3] int? 3) ;; => true
(seq-regex [1 2 3] int? [1 4]) ;; => true

Predicates must be specific enough so that they aren’t consumed further than you intend. This is treacherous when, e.g., converting to string. Here’s possibly surprising failing example:

(seq-regex [:a :b :c 'fo 'br 'bz] #(= 2 (count (str %))) 3 symbol? 3)
;; => false

'fo, 'br, and 'bz all satisfy length=2 when converted to string, leaving symbol? no values to test. Instead, insert a guarding predicate.

(seq-regex [:a :b :c 'fo 'br 'bz] #(and (keyword? %)
                                        (= 2 (count (str %)))) 3 symbol? 3)
;; => true

sore-thumb

(sore-thumb data spec)(sore-thumb data spec replacement)

Prints *out* a version of data and spec that highlights where the datums+predicates invalidate. Non-invalids (i.e., datums that satisfy their own predicates) are de-emphasized with replacement, defaulting to '_.

'… is also kinda nice.

Examples:

;; default replacement '_
(with-out-str (sore-thumb [42 :foo 22/7] [int? symbol? ratio?]))
;; printed to *out*: data: [_ :foo _] spec: [_ symbol? _]

;; optional replacement '…
(with-out-str (sore-thumb {:a 42 :b 'foo :c 22/7} {:a int? :b keyword? :c ratio?} '…))
;; printed to *out*: data: {:a …, :b foo, :c …} spec: {:a …, :b keyword?, :c …}

spec-from-data

(spec-from-data data)(spec-from-data data clamp-at)

Given heterogeneous, arbitrarily-nested collection data, create a scalar specification whose predicates validate. Non-terminating sequences are clamped at clamp-at, defaults to 7.

Examples:

(spec-from-data [42 [:foo] 22/7]) ;; => [int? [keyword?] ratio?]
(spec-from-data {:a 42 :b {:c 'foo :d 22/7}}) ;; => {:a int?, :b {:c symbol?, :d ratio?}}

;; non-terminating data sequence, optional clamp-at supplied
(spec-from-data [:foo (cycle [42 22/7 'baz])] 5) ;; => [keyword? [int? ratio? symbol? int? ratio?]]

swap-non-predicates

(swap-non-predicates spec)(swap-non-predicates spec pred)

Returns a new scalar specification spec with all non-predicate elements swapped for any? (default) or optional supplied predicate pred.

Examples:

;; default predicate replacement
(swap-non-predicates [int? 99 ratio?]) ;; => [int? any? ratio?]

;; non-default predicate replacement
(swap-non-predicates {:a int? :b 99 :c 'foo} number?) ;; => {:a int?, :b number?, :c number?}

thoroughly-valid-collections?

(thoroughly-valid-collections? data spec)

Given a heterogeneous, arbitrarily-nested data structure data and collection specification spec, returns true if every collection contained in data has a corresponding predicate in spec, and every collection satisfies its predicate.

See valid-collections?, collections-without-predicates, and thoroughly-valid?.

Examples:

;; all collections have satisfied predicates
(thoroughly-valid-collections? [11 {:x 22/7 :y 'foo}] [vector? {:is-map? map?}])
;; => true

;; all collections have predicates, but not all satisfied
(thoroughly-valid-collections? [11 {:x 22/7 :y 'foo}] [list? {:is-map? map?}])
;; => false

;; all predicates satisfied, but one collection is missing a predicate
(thoroughly-valid-collections? [11 {:x 22/7 :y 'foo}] [{:is-map? map?}])
;; => false

thoroughly-valid-scalars?

(thoroughly-valid-scalars? data spec)

Given a heterogeneous, arbitrarily-nested data structure data and scalar specification spec, returns true if every scalar contained in data has a corresponding predicate in spec, and every scalar satisfies its predicate.

See valid-scalars?, scalars-without-predicates, and thoroughly-valid?.

Examples:

;; all scalars have satisfied predicates
(thoroughly-valid-scalars? [11 {:x 22/7 :y 'foo}] [int? {:x ratio? :y symbol?}])
;; => true

;; all scalars have predicates, but not all satisfied
(thoroughly-valid-scalars? [11 {:x 22/7 :y 'foo}] [char? {:x ratio? :y symbol?}])
;; => false

;; all predicates satisfied, but one scalar is missing a predicate
(thoroughly-valid-scalars? [11 {:x 22/7 :y 'foo}] [int? {:x ratio?}])
;; => false

thoroughly-valid?

(thoroughly-valid? data scalar-spec collection-spec)

Given a heterogeneous, arbitrarily-nested data structure data, returns true if every scalar in data has a predicate in scalar-spec and every collection has at least one predicate in collection-spec, and all predicates are satisfied.

See valid?, scalars-without-predicates, and collections-without-predicates.

Examples:

;; all scalars and colls have satisfied predicates
(thoroughly-valid? [11 {:x 22/7 :y 'foo}] [int? {:x ratio? :y symbol?}] [vector? {:is-map? map?}])
;; => true

;; all scalars and colls have predicates, but not all satisfied
(thoroughly-valid? [11 {:x 22/7 :y 'foo}] [char? {:x ratio? :y symbol?}] [vector? {:is-map? map?}])
;; => false

;; all predicates satisfied, but one scalar is missing a predicate
(thoroughly-valid? [11 {:x 22/7 :y 'foo}] [int? {:x ratio?}] [vector? {:is-map? map?}])
;; => false

;; all predicates satisfied, but one coll is missing a predicate
(thoroughly-valid? [11 {:x 22/7 :y 'foo}] [int? {:x ratio? :y symbol?}] [{:is-map? map?}])
;; => false

unfindable-generators

(unfindable-generators spec)

Given scalar specification spec, check that a random sample generator can be located for each predicate-like thing within spec. Returns a sequence of any thingy that does not, and a path to its location within spec.

  • Many basic predicates such as int?, string?, ratio?, etc., are provided by clojure.test.check.generators.
  • Sets and regular expressions are competent generators.
  • Compound predicates such as #(and (number? %) (< % 99)) may be explicitly supplied with a custom generator located within its metadata map at key :speculoos/predicate->generator.

Note: Being able to locate a random sample generator does not imply that it works properly.

;; all good generators
(unfindable-generators [int? #{:red :green :blue} #"foo" (with-meta #(int? %) {:speculoos/predicate->generator #(rand-int 99)})])
;; => []

;; all bad generators
(unfindable-generators {:no-meta-data #(int? %)
                        :no-generator +
                        :improper-key (with-meta #(int? %) {:speculoos/oops #(rand-int 99)})})
;; => [{:path [:no-meta-data], :value #fn--34610]}
;;     {:path [:no-generator], :value clojure.core/+}
;;     {:path [:improper-key], :value #function[clojure.lang.AFunction/1]]}]

validate-predicate->generator

(validate-predicate->generator f)(validate-predicate->generator f n)

Repeatedly invoke predicate function f, with sample arguments produced by generator located at key :speculoos/predicate->generator in the function metadata. n invocations (default 7). Returns a sequence of vectors containing the generated value and the result of the validation.

Useful for checking manually-injected generators consulted by data-from-spec.

Example:

;; basic integer predicate and generator
(validate-predicate->generator (with-meta #(int? %) {:speculoos/predicate->generator #(rand-int 99)}))
;; => ([64 true] [84 true] [21 true] [74 true] [88 true] [13 true] [33 true])

;; compound integer predicate with compound integer generator
(require '[clojure.test.check.generators :as gen])

;; positive, even integers, less than one hundred; warm up generator by 25 calls and peeling off last
(def gen-1 #(last (gen/sample (gen/such-that even? (gen/large-integer* {:min 0 :max 99})) 25)))

(validate-predicate->generator (with-meta #(and (int? %) (pos? %) (even? %) (< % 100)) {:speculoos/predicate->generator gen-1}))
;; => ([72 true] [52 true] [2 true] [82 true] [64 true] [56 true] [10 true])

;; same thing, but intentionally wrong generator that produces odd integers
(def incorrect-gen-1 #(last (gen/sample (gen/such-that odd? (gen/large-integer* {:min 0 :max 99})) 25)))

(validate-predicate->generator (with-meta #(and (int? %) (pos? %) (even? %) (< % 100)) {:speculoos/predicate->generator incorrect-gen-1}))
;; => ([61 false] [57 false] [3 false] [97 false] [53 false] [77 false] [63 false])

;; validating regular expression generator: capital Z, followed by one to three digits, followed by a, b, or c
(def re-spec #"Z\d{1,3}[abc]")

;; re-rand generates random strings that satisfy a given regex [https://github.com/weavejester/re-rand]
(require '[re-rand :refer [re-rand]])

(validate-predicate->generator (with-meta #(boolean (re-matches re-spec %)) {:speculoos/predicate->generator #(re-rand re-spec)}))
;; (["Z919b" true] ["Z99b" true] ["Z5a" true] ["Z210a" true] ["Z711c" true] ["Z319a" true] ["Z81c" true])