(ns schema.experimental.generators "(Very) experimental support for compiling schemas to test.check generators. To use it, you must provide your own test.check dependency. TODO: add cljs support." {:deprecated "1.1.0"} (:require [clojure.test.check.generators :as generators] [schema.spec.core :as spec] schema.spec.collection schema.spec.leaf schema.spec.variant [schema.core :as s] [schema.macros :as macros])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Private helpers for composite schemas (defn g-by [f & args] (generators/fmap (partial apply f) (apply generators/tuple args))) (defn g-apply-by [f args] (generators/fmap f (apply generators/tuple args))) (defn- sub-generator [{:keys [schema]} {:keys [subschema-generator ^java.util.Map cache] :as params}] (spec/with-cache cache schema (fn [d] (#'generators/make-gen (fn [r s] (generators/call-gen @d r (quot s 2))))) (fn [] (subschema-generator schema params)))) ;; Helpers for collections (declare elements-generator) (defn element-generator [e params] (if (vector? e) (case (first e) :schema.spec.collection/optional (generators/one-of [(generators/return nil) (elements-generator (next e) params)]) :schema.spec.collection/remaining (do (macros/assert! (= 2 (count e)) "remaining can have only one schema.") (generators/vector (sub-generator (second e) params)))) (generators/fmap vector (sub-generator e params)))) (defn elements-generator [elts params] (->> elts (map #(element-generator % params)) (apply generators/tuple) (generators/fmap (partial apply concat)))) (defprotocol CompositeGenerator (composite-generator [s params])) (extend-protocol CompositeGenerator schema.spec.variant.VariantSpec (composite-generator [s params] (generators/such-that (fn [x] (let [pre (.-pre ^schema.spec.variant.VariantSpec s) post (.-post ^schema.spec.variant.VariantSpec s)] (not (or (pre x) (and post (post x)))))) (generators/one-of (for [o (macros/safe-get s :options)] (if-let [g (:guard o)] (generators/such-that g (sub-generator o params)) (sub-generator o params)))))) ;; TODO: this does not currently capture proper semantics of maps with ;; both specific keys and key schemas that can override them. schema.spec.collection.CollectionSpec (composite-generator [s params] (generators/such-that (complement (.-pre ^schema.spec.collection.CollectionSpec s)) (generators/fmap (:constructor s) (elements-generator (:elements s) params)))) schema.spec.leaf.LeafSpec (composite-generator [s params] (macros/assert! false "You must provide a leaf generator for %s" s))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;; Public (def Schema "A Schema for Schemas" (s/protocol s/Schema)) (def Generator "A test.check generator" s/Any) (def LeafGenerators "A mapping from schemas to generating functions that should be used." (s/=> (s/maybe Generator) Schema)) (def +primitive-generators+ {Double generators/double ;; using unchecked-float here will unfortunately generate a lot of ;; infinities, since lots of doubles are out of the float range Float (generators/fmap unchecked-float generators/double) Long generators/large-integer Integer (generators/fmap unchecked-int generators/large-integer) Short (generators/fmap unchecked-short generators/large-integer) Character (generators/fmap unchecked-char generators/large-integer) Byte (generators/fmap unchecked-byte generators/large-integer) Boolean generators/boolean}) (def +simple-leaf-generators+ (merge +primitive-generators+ {s/Str generators/string-ascii s/Bool generators/boolean s/Num (generators/one-of [generators/large-integer generators/double]) s/Int (generators/one-of [generators/large-integer (generators/fmap unchecked-int generators/large-integer) (generators/fmap bigint generators/large-integer)]) s/Keyword generators/keyword clojure.lang.Keyword generators/keyword s/Symbol (generators/fmap (comp symbol name) generators/keyword) Object generators/any s/Any generators/any s/Uuid generators/uuid s/Inst (generators/fmap (fn [ms] (java.util.Date. ms)) generators/int)} (into {} (for [[f ctor c] [[doubles double-array Double] [floats float-array Float] [longs long-array Long] [ints int-array Integer] [shorts short-array Short] [chars char-array Character] [bytes byte-array Byte] [booleans boolean-array Boolean]]] [f (generators/fmap ctor (generators/vector (macros/safe-get +primitive-generators+ c)))])))) (defn eq-generators [s] (when (instance? schema.core.EqSchema s) (generators/return (.-v ^schema.core.EqSchema s)))) (defn enum-generators [s] (when (instance? schema.core.EnumSchema s) (let [vs (vec (.-vs ^schema.core.EqSchema s))] (generators/fmap #(nth vs %) (generators/choose 0 (dec (count vs))))))) (defn default-leaf-generators [leaf-generators] (some-fn leaf-generators +simple-leaf-generators+ eq-generators enum-generators)) (defn always [x] (generators/return x)) (def GeneratorWrappers "A mapping from schemas to wrappers that should be used around the default generators." (s/=> (s/maybe (s/=> Generator Generator)) Schema)) (defn such-that "Helper wrapper that filters to values that match predicate." [f] (partial generators/such-that f)) (defn fmap "Helper wrapper that maps f over all values." [f] (partial generators/fmap f)) (defn merged "Helper wrapper that merges some keys into a schema" [m] (fmap #(merge % m))) (s/defn generator :- Generator "Produce a test.check generator for schema. leaf-generators must return generators for all leaf schemas, and can also return generators for non-leaf schemas to override default generation logic. constraints is an optional mapping from schema to wrappers for the default generators, which can impose constraints, fix certain values, etc." ([schema] (generator schema {})) ([schema leaf-generators] (generator schema leaf-generators {})) ([schema :- Schema leaf-generators :- LeafGenerators wrappers :- GeneratorWrappers] (let [leaf-generators (default-leaf-generators leaf-generators) gen (fn [s params] ((or (wrappers s) identity) (or (leaf-generators s) (composite-generator (s/spec s) params))))] (generators/fmap (s/validator schema) (gen schema {:subschema-generator gen :cache (java.util.IdentityHashMap.)}))))) (s/defn sample :- [s/Any] "Sample k elements from generator." [k & generator-args] (generators/sample (apply generator generator-args) k)) (s/defn generate "Sample a single element of low to moderate size." [& generator-args] (generators/generate (apply generator generator-args) 10))