From 2a4a08e83e4726e623ce29082bcf5f646008bd8e Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Wed, 26 Mar 2025 19:49:06 +0000 Subject: [PATCH 01/22] add :andn, more flexible :and parsing, parse optimizations --- README.md | 25 +++- src/malli/core.cljc | 247 ++++++++++++++++++++++++++++++-------- test/malli/core_test.cljc | 102 ++++++++++++++-- 3 files changed, 312 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index d5493d725..ea7309ada 100644 --- a/README.md +++ b/README.md @@ -2493,7 +2493,7 @@ Schemas can be used to parse values using `m/parse` and `m/parser`: ; :value "Hello, world of data"}}]}}}]}}} ``` -Parsing returns tagged values for `:orn`, `:catn`, `:altn` and `:multi`. +Parsing returns tagged values for `:orn`, `:catn`, `:altn`, `:andn` and `:multi`. ```clojure (def Multi @@ -2508,6 +2508,29 @@ Parsing returns tagged values for `:orn`, `:catn`, `:altn` and `:multi`. ; => #malli.core.Tag{:key :malli.core/default, :value {:type "sized", :size 1}} ``` +### Parsing `:and` + +The `:and` schema is unusual in that it parses multiple schemas and yet only returns the results of parsing one of them. + +This works seamlessly, unless more than one conjunct's parser transforms its input. +Examples of schemas that can transform their input are `:orn` (returns `Tag`), +`:catn` (returns `Tags`), and any composite schema such as `:map` (recursively transforms children). + +The error `:malli.core/and-schema-multiple-transforming-parsers` is thrown if the transforming +parser cannot be picked automatically. There are several ways to resolve this. + +If you know a single conjunct should exclusively parse the input, use the `:parse` property +to identify the conjunct by index. + +[:and [:and [:orn [:l :int] [:r :boolean]]]] +[:and [:and [:orn [:l :int] [:r :boolean]]]] + +To opt-out of parsing any further levels of this schema, use the `:parse :none` property. + +To parse all conjuncts, you must migrate the schema to `:andn`. This involves tagging each conjunct +with syntax like `:orn` and `:map`. The results of parsing will be wrapped in `Tags`. +Only the left-most child will be unparsed, useful if you plan to modify the results of parsing. + ## Unparsing values The inverse of parsing, using `m/unparse` and `m/unparser`: diff --git a/src/malli/core.cljc b/src/malli/core.cljc index fead6e24b..e67115ce1 100644 --- a/src/malli/core.cljc +++ b/src/malli/core.cljc @@ -96,6 +96,9 @@ (-distributive-schema? [this]) (-distribute-to-children [this f options])) +(defprotocol ParserInfo + (-parser-info [this])) + (defn -ref-schema? [x] (#?(:clj instance?, :cljs implements?) malli.core.RefSchema x)) (defn -entry-parser? [x] (#?(:clj instance?, :cljs implements?) malli.core.EntryParser x)) (defn -entry-schema? [x] (#?(:clj instance?, :cljs implements?) malli.core.EntrySchema x)) @@ -115,6 +118,14 @@ (-distribute-to-children [this _ _] (throw (ex-info "Not distributive" {:schema this}))) + ParserInfo + (-parser-info [this] + (when (schema? this) + (if (-ref-schema? this) + (-parser-info (-deref this)) + (when (-> this -parent -type-properties ::simple-parser) + {:simple-parser true})))) + RegexSchema (-regex-op? [_] false) @@ -704,6 +715,10 @@ ;; Schemas ;; +(defn -simple-parser [s] + (let [validator (-validator s)] + (fn [x] (if (validator x) x ::invalid)))) + (defn -simple-schema [props] (let [{:keys [type type-properties pred property-pred min max from-ast to-ast compile] :or {min 0, max 0, from-ast -from-value-ast, to-ast -to-type-ast}} props] @@ -717,7 +732,7 @@ (-from-ast [parent ast options] (from-ast parent ast options)) IntoSchema (-type [_] type) - (-type-properties [_] type-properties) + (-type-properties [_] (assoc type-properties ::simple-parser true)) (-properties-schema [_ _]) (-children-schema [_ _]) (-into-schema [parent properties children options] @@ -738,9 +753,7 @@ (let [validator (-validator this)] (fn explain [x in acc] (if-not (validator x) (conj acc (miu/-error path in this x)) acc)))) - (-parser [this] - (let [validator (-validator this)] - (fn [x] (if (validator x) x ::invalid)))) + (-parser [this] (-simple-parser this)) (-unparser [this] (-parser this)) (-transformer [this transformer method options] (-intercepting (-value-transformer transformer this method options))) @@ -778,13 +791,28 @@ (-type-properties [_]) (-properties-schema [_ _]) (-children-schema [_ _]) - (-into-schema [parent properties children options] + (-into-schema [parent {:keys [tags] :as properties} children options] (-check-children! :and properties children 1 nil) (let [children (-vmap #(schema % options) children) form (delay (-simple-form parent properties children -form options)) cache (-create-cache options) - ->parser (fn [f m] (let [parsers (m (-vmap f children))] - #(reduce (fn [x parser] (miu/-map-invalid reduced (parser x))) % parsers)))] + transforming-parser (delay + (let [transforming-parsers (or (when-some [[_ i] (find properties :parse)] + (or (when (= :none i) + []) + (when-not (and (nat-int? i) (< i (count children))) + (-fail! ::and-schema-invalid-parse-property {:schema @form})) + [i])) + (into [] + (keep-indexed + (fn [i c] + (when-not (-> c -parser-info :simple-parser) + i))) + children))] + (when (next transforming-parsers) + (-fail! ::and-schema-multiple-transforming-parsers {:schema @form})) + (peek transforming-parsers))) + ->parsers (fn [f] (into [] (map-indexed (fn [i c] (if (= @transforming-parser i) (f c) (-simple-parser c)))) children))] ^{:type ::schema} (reify Schema @@ -793,8 +821,37 @@ (-explainer [_ path] (let [explainers (-vmap (fn [[i c]] (-explainer c (conj path i))) (map-indexed vector children))] (fn explain [x in acc] (reduce (fn [acc' explainer] (explainer x in acc')) acc explainers)))) - (-parser [_] (->parser -parser seq)) - (-unparser [_] (->parser -unparser rseq)) + (-parser [this] + ;; non-iteratively parse x left-to-right. return result of transforming parser, or x. + (let [pi @transforming-parser + parsers (->parsers -parser) + nchildren (count children)] + (fn [x] + (reduce (fn [acc i] + (let [x' ((nth parsers i) x)] + (if (miu/-invalid? x') + (reduced ::invalid) + (if (= pi i) + x' + acc)))) + x (range nchildren))))) + (-unparser [this] + ;; unparse x' with transforming parser (if any), then non-iteratively unparse x with remaining parsers, left-to-right + ;; return x if all results are equal. + (let [pi @transforming-parser + unparsers (->parsers -unparser) + unparser (get unparsers pi identity) + nchildren (count children)] + (fn [x'] + (let [x (unparser x')] + (reduce (fn [acc i] + (if (= pi i) + acc + (let [x' ((nth unparsers i) x)] + (if (miu/-invalid? x') + (reduced ::invalid) + x')))) + x (range nchildren)))))) (-transformer [this transformer method options] (-parent-children-transformer this children transformer method options)) (-walk [this walker path options] (-walk-indexed this walker path options)) @@ -808,7 +865,83 @@ LensSchema (-keep [_]) (-get [_ key default] (get children key default)) - (-set [this key value] (-set-assoc-children this key value))))))) + (-set [this key value] (-set-assoc-children this key value)) + ParserInfo + (-parser-info [_] (if-some [i @transforming-parser] (-parser-info (nth children i)) {}))))))) + +(defn -andn-schema [] + ^{:type ::into-schema} + (reify + AST + (-from-ast [parent ast options] (-from-entry-ast parent ast options)) + IntoSchema + (-type [_] :andn) + (-type-properties [_]) + (-properties-schema [_ _]) + (-children-schema [_ _]) + (-into-schema [parent properties children options] + (-check-children! :andn properties children 1 nil) + (let [entry-parser (-create-entry-parser children {:naked-keys true} options) + form (delay (-create-entry-form parent properties entry-parser options)) + cache (-create-cache options)] + ^{:type ::schema} + (reify + AST + (-to-ast [this _] (-entry-ast this (-entry-keyset entry-parser))) + Schema + (-validator [this] (miu/-every-pred (-vmap (fn [[_ _ c]] (-validator c)) (-children this)))) + (-explainer [this path] + (let [explainers (-vmap (fn [[k _ c]] (-explainer c (conj path k))) (-children this))] + (fn explain [x in acc] (reduce (fn [acc' explainer] (explainer x in acc')) acc explainers)))) + (-parser [this] + (let [k+parsers (-vmap (fn [[k _ c]] [k (-parser c)]) (-children this))] + (fn [x] + (let [tags (reduce (fn [acc [k parser]] + (let [x' (parser x)] + (if (miu/-invalid? x') + (reduced ::invalid) + (assoc acc k x')))) + {} k+parsers)] + (if (miu/-invalid? tags) + ::invalid + (->Tags tags)))))) + (-unparser [this] + ;; only the left-most child provided in tags is unparsed. the remaining values are ignored. + ;; the unparsed value is checked against the remaining children. + ;; if you want to modify a particular conjunct's unparsed value, you should remove all others. + (let [ks (-vmap #(nth % 0) (-children this)) + validators (into {} (map (fn [[k _ c]] [k (-validator c)])) (-children this)) + unparsers (into {} (map (fn [[k _ c]] [k (-unparser c)])) (-children this)) + nchildren (count children)] + (fn [tags] + (if-some [values (when (tags? tags) (not-empty (:values tags)))] + (if (every? validators (keys values)) + (let [[k x'] (some #(find values %) ks) + x ((unparsers k) x')] + (if (and (not (miu/-invalid? x)) + (every? #(or (= k %) ((validators k) x)) ks)) + x + ::invalid)) + ::invalid) + ::invalid)))) + (-transformer [this transformer method options] + ;FIXME !!! + (-or-transformer this transformer (-vmap #(nth % 2) (-children this)) method options)) + (-walk [this walker path options] (-walk-entries this walker path options)) + (-properties [_] properties) + (-options [_] options) + (-children [_] (-entry-children entry-parser)) + (-parent [_] parent) + (-form [_] @form) + EntrySchema + (-entries [_] (-entry-entries entry-parser)) + (-entry-parser [_] entry-parser) + Cached + (-cache [_] cache) + LensSchema + (-keep [_]) + (-get [this key default] (-get-entries this key default)) + (-set [this key value] (-set-entries this key value))))))) (defn -or-schema [] ^{:type ::into-schema} @@ -852,7 +985,9 @@ LensSchema (-keep [_]) (-get [_ key default] (get children key default)) - (-set [this key value] (-set-assoc-children this key value))))))) + (-set [this key value] (-set-assoc-children this key value)) + ParserInfo + (-parser-info [_] {:simple-parser (every? (comp :simple-parser -parser-info) children)})))))) (defn -orn-schema [] ^{:type ::into-schema} @@ -922,7 +1057,7 @@ (-from-ast [parent ast options] (-from-child-ast parent ast options)) IntoSchema (-type [_] :not) - (-type-properties [_]) + (-type-properties [_] {::simple-parser true}) (-properties-schema [_ _]) (-children-schema [_ _]) (-into-schema [parent properties children options] @@ -940,9 +1075,7 @@ (let [validator (-validator this)] (fn explain [x in acc] (if-not (validator x) (conj acc (miu/-error (conj path 0) in this x)) acc)))) - (-parser [this] - (let [validator (-validator this)] - (fn [x] (if (validator x) x ::invalid)))) + (-parser [this] (-simple-parser this)) (-unparser [this] (-parser this)) (-transformer [this transformer method options] (-parent-children-transformer this children transformer method options)) @@ -1047,12 +1180,13 @@ (if optional m (reduced ::invalid)))))) @explicit-children) default-parser - (cons (fn [m] - (let [m' (default-parser - (reduce (fn [acc k] (dissoc acc k)) m (keys keyset)))] - (if (miu/-invalid? m') - (reduced m') - (merge (select-keys m (keys keyset)) m'))))) + (cons (let [simple (-> @default-schema -parser-info :simple-parser boolean)] + (fn [m] + (let [m' (default-parser + (reduce (fn [acc k] (dissoc acc k)) m (keys keyset)))] + (if (miu/-invalid? m') + (reduced m') + (if simple m (merge (select-keys m (keys keyset)) m'))))))) closed (cons (fn [m] (reduce @@ -1148,7 +1282,9 @@ LensSchema (-keep [_] true) (-get [this key default] (-get-entries this key default)) - (-set [this key value] (-set-entries this key value)))))))) + (-set [this key value] (-set-entries this key value)) + ParserInfo + (-parser-info [_] {:simple-parser (every? (comp :simple-parser -parser-info) (-entry-children entry-parser))}))))))) (defn -map-of-schema ([] @@ -1170,18 +1306,20 @@ form (delay (-simple-form parent properties children -form options)) cache (-create-cache options) validate-limits (-validate-limits min max) + simple-parser (delay (every? (comp :simple-parser -parser-info) children)) ->parser (fn [f] (let [key-parser (f key-schema) - value-parser (f value-schema)] + value-parser (f value-schema) + simple @simple-parser] (fn [x] (if (map? x) (reduce-kv (fn [acc k v] (let [k* (key-parser k) v* (value-parser v)] - ;; OPTIMIZE: Restore `identical?` check + NOOP (if (or (miu/-invalid? k*) (miu/-invalid? v*)) (reduced ::invalid) - (assoc acc k* v*)))) - (empty x) x) + (cond-> acc + (not simple) (assoc k* v*))))) + (cond-> x (not simple) empty) x) ::invalid))))] ^{:type ::schema} (reify @@ -1238,7 +1376,9 @@ LensSchema (-keep [_]) (-get [_ key default] (get children key default)) - (-set [this key value] (-set-assoc-children this key value)))))))) + (-set [this key value] (-set-assoc-children this key value)) + ParserInfo + (-parser-info [_] {:simple-parser @simple-parser}))))))) ;; also doubles as a predicate for the :every schema to bound the number ;; of elements to check, so don't add potentially-infinite countable things like seq's. @@ -1285,7 +1425,8 @@ validate-limits (if bounded (-validate-bounded-limits (c/min bounded (or max bounded)) min max) (-validate-limits min max)) - ->parser (fn [f g] (let [child-parser (f schema)] + ->parser (fn [f g] (let [child-parser (f schema) + simple (-> schema -parser-info :simple-parser boolean)] (fn [x] (cond (not (fpred x)) ::invalid @@ -1301,12 +1442,15 @@ (let [x' (reduce (fn [acc v] (let [v' (child-parser v)] - (if (miu/-invalid? v') (reduced ::invalid) (conj acc v')))) - [] x)] + (if (miu/-invalid? v') + (reduced ::invalid) + (cond-> acc + (not simple) (conj v'))))) + (if simple x []) x)] (cond (miu/-invalid? x') x' g (g x') - fempty (into fempty x') + (and fempty (or (not simple) (not (fpred x')))) (into fempty x') :else x')))))))] ^{:type ::schema} (reify @@ -1359,7 +1503,9 @@ LensSchema (-keep [_] true) (-get [_ _ _] schema) - (-set [this _ value] (-set-children this [value])))))))))) + (-set [this _ value] (-set-children this [value])) + ParserInfo + (-parser-info [_] (-parser-info schema)))))))))) (defn -tuple-schema ([] @@ -1431,7 +1577,9 @@ LensSchema (-keep [_] true) (-get [_ key default] (get children key default)) - (-set [this key value] (-set-assoc-children this key value)))))))) + (-set [this key value] (-set-assoc-children this key value)) + ParserInfo + (-parser-info [_] (every? (comp :simple-parser -parser-info) children)))))))) (defn -enum-schema [] ^{:type ::into-schema} @@ -1440,7 +1588,7 @@ (-from-ast [parent ast options] (-into-schema parent (:properties ast) (:values ast) options)) IntoSchema (-type [_] :enum) - (-type-properties [_]) + (-type-properties [_] {::simple-parser true}) (-into-schema [parent properties children options] (-check-children! :enum properties children 1 nil) (let [children (vec children) @@ -1458,7 +1606,7 @@ (let [validator (-validator this)] (fn explain [x in acc] (if-not (validator x) (conj acc (miu/-error path in this x)) acc)))) - (-parser [_] (fn [x] (if (contains? schema x) x ::invalid))) + (-parser [this] (-simple-parser this)) (-unparser [this] (-parser this)) ;; TODO: should we try to derive the type from values? e.g. [:enum 1 2] ~> int? (-transformer [this transformer method options] @@ -1483,7 +1631,7 @@ (-from-ast [parent ast options] (-from-value-ast parent ast options)) IntoSchema (-type [_] :re) - (-type-properties [_]) + (-type-properties [_] {::simple-parser true}) (-properties-schema [_ _]) (-children-schema [_ _]) (-into-schema [parent properties [child :as children] options] @@ -1511,9 +1659,7 @@ (conj acc (miu/-error path in this x (:type (ex-data e)))))))) (-transformer [this transformer method options] (-intercepting (-value-transformer transformer this method options))) - (-parser [this] - (let [valid? (-validator this)] - (fn [x] (if (valid? x) x ::invalid)))) + (-parser [this] (-simple-parser this)) (-unparser [this] (-parser this)) (-walk [this walker path options] (-walk-leaf this walker path options)) (-properties [_] properties) @@ -1535,7 +1681,7 @@ (-from-ast [parent ast options] (-from-value-ast parent ast options)) IntoSchema (-type [_] :fn) - (-type-properties [_]) + (-type-properties [_] {::simple-parser true}) (-into-schema [parent properties children options] (-check-children! :fn properties children 1 1) (let [children (vec children) @@ -1556,9 +1702,7 @@ acc) (catch #?(:clj Exception, :cljs js/Error) e (conj acc (miu/-error path in this x (:type (ex-data e)))))))) - (-parser [this] - (let [validator (-validator this)] - (fn [x] (if (validator x) x ::invalid)))) + (-parser [this] (-simple-parser this)) (-unparser [this] (-parser this)) (-transformer [this transformer method options] (-intercepting (-value-transformer transformer this method options))) @@ -1620,7 +1764,9 @@ (-get [_ key default] (if (= 0 key) schema default)) (-set [this key value] (if (= 0 key) (-set-children this [value]) - (-fail! ::index-out-of-bounds {:schema this, :key key})))))))) + (-fail! ::index-out-of-bounds {:schema this, :key key}))) + ParserInfo + (-parser-info [_] (-parser-info schema))))))) (defn -multi-schema ([] @@ -1867,7 +2013,7 @@ guard (conj (from-ast guard))) options)) IntoSchema (-type [_] :=>) - (-type-properties [_]) + (-type-properties [_] {::simple-parser true}) (-into-schema [parent properties children {::keys [function-checker] :as options}] (-check-children! :=> properties children 2 3) (let [[input output guard :as children] (-vmap #(schema % options) children) @@ -1903,9 +2049,7 @@ (let [validator (-validator this)] (fn explain [x in acc] (if-not (validator x) (conj acc (miu/-error path in this x)) acc))))) - (-parser [this] - (let [validator (-validator this)] - (fn [x] (if (validator x) x ::invalid)))) + (-parser [this] (-simple-parser this)) (-unparser [this] (-parser this)) (-transformer [_ _ _ _]) (-walk [this walker path options] (-walk-indexed this walker path options)) @@ -1955,7 +2099,7 @@ ^{:type ::into-schema} (reify IntoSchema (-type [_] :function) - (-type-properties [_]) + (-type-properties [_] {::simple-parser true}) (-properties-schema [_ _]) (-children-schema [_ _]) (-into-schema [parent properties children {::keys [function-checker] :as options}] @@ -1985,9 +2129,7 @@ (let [validator (-validator this)] (fn explain [x in acc] (if-not (validator x) (conj acc (miu/-error path in this x)) acc))))) - (-parser [this] - (let [validator (-validator this)] - (fn [x] (if (validator x) x ::invalid)))) + (-parser [this] (-simple-parser this)) (-unparser [this] (-parser this)) (-transformer [_ _ _ _]) (-walk [this walker path options] (-walk-indexed this walker path options)) @@ -2720,6 +2862,7 @@ (defn base-schemas [] {:and (-and-schema) + :andn (-andn-schema) :or (-or-schema) :orn (-orn-schema) :not (-not-schema) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 617943971..1bed7dbc8 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -18,12 +18,12 @@ (defn with-schema-forms [result] (some-> result (update :schema m/form) - (update :errors (partial map (fn [error] - (-> error - (update :schema m/form) - (update :type (fnil identity nil)) - (update :message (fnil identity nil)) - (dissoc :check))))))) + (update :errors (partial mapv (fn [error] + (-> error + (update :schema m/form) + (update :type (fnil identity nil)) + (update :message (fnil identity nil)) + (dissoc :check))))))) (defn as-data [x] (walk/prewalk (fn [x] (cond-> x (m/schema? x) (m/form))) x)) @@ -2369,7 +2369,8 @@ :json-schema/type "integer" :json-schema/format "int64" :json-schema/minimum 6 - :gen/gen generate-over6} + :gen/gen generate-over6 + ::m/simple-parser true} (m/type-properties over6))) (is (= {:json-schema/example 42} (m/properties over6)))))) @@ -2400,7 +2401,8 @@ :decode/string mt/-string->long :json-schema/type "integer" :json-schema/format "int64" - :json-schema/minimum 6} + :json-schema/minimum 6 + ::m/simple-parser true} (m/type-properties schema))) (is (= {:value 6} (m/properties schema)))))) @@ -2417,7 +2419,8 @@ :decode/string mt/-string->long :json-schema/type "integer" :json-schema/format "int64" - :json-schema/minimum 42} + :json-schema/minimum 42 + ::m/simple-parser true} (m/type-properties schema))) (is (= {:value 42} (m/properties schema))))))))) @@ -3581,3 +3584,84 @@ (is (not (m/validate [:sequential {:min 11} :int] (eduction identity (range 10))))) (is (not (m/validate [:seqable {:min 11} :int] (eduction identity (range 10))))) (is (nil? (m/explain [:sequential {:min 9} :int] (eduction identity (range 10)))))) + +(deftest and-complex-parser-test + (is (= {} (m/parse [:and :map [:fn map?]] {}))) + (is (= {} (m/parse [:and [:fn map?] :map] {}))) + (is (= #malli.core.Tag{:key :left, :value 1} (m/parse [:and [:orn [:left :int] [:right :int]] [:fn number?]] 1))) + (is (= #malli.core.Tag{:key :left, :value 1} (m/parse [:and [:fn number?] [:orn [:left :int] [:right :int]]] 1))) + (is (= 1 (m/parse [:and {:parse :none} [:fn number?] [:orn [:left :int] [:right :int]]] 1))) + (is (= 1 (m/parse [:and :int [:or :int :boolean]] 1))) + (is (= 1 (m/parse [:and [:or :int :boolean] :int] 1))) + (is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and :int [:orn [:int :int] [:boolean :boolean]]] 1))) + (is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and [:orn [:int :int] [:boolean :boolean]] :int] 1))) + (is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and [:and [:orn [:int :int] [:boolean :boolean]] :int] :int] 1))) + (is (= #malli.core.Tag{:key :l, :value #malli.core.Tag{:key :int, :value 1}} + (m/parse [:and + [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] + :int] 1))) + (is (= 1 + (m/parse [:and + {:parse :none} + [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] + [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]] + 1))) + (is (= #malli.core.Tag{:key :l, :value #malli.core.Tag{:key :int, :value 1}} + (m/parse [:and + {:parse 0} + [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] + [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]] + 1))) + (is (= #malli.core.Tag{:key :r, :value #malli.core.Tag{:key :int, :value 1}} + (m/parse [:and + {:parse 1} + [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] + [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]] + 1))) + (let [s [:and [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] :int]] + (is (= 1 (->> 1 (m/parse s) (m/unparse s))))) + (let [s [:and + {:parse 1} + [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] + [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]]] + (is (= 1 (->> 1 (m/parse s) (m/unparse s))))) + (is (m/parser [:and [:map] [:map]])) + (is (m/parser [:and [:map [:left [:orn [:one :int]]]] [:map]])) + (is (m/parser [:and [:map] [:map [:left [:orn [:one :int]]]]])) + (is (thrown-with-msg? + #?(:clj Exception, :cljs js/Error) + #":malli\.core/and-schema-multiple-transforming-parsers" + (m/parser [:and [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]))) + (is (-> (m/schema [:vector :int]) m/-parser-info :simple-parser)) + (is (-> (m/schema [:vector [:orn [:one :int]]]) m/-parser-info :simple-parser not))) + +(deftest andn-test + (is (= {:schema [:andn [:m :map] [:v [:vector :any]]], + :value {}, + :errors + [{:path [:v], + :in [], + :schema [:vector :any], + :value {}, + :type :malli.core/invalid-type, + :message nil}]} + (with-schema-forms + (m/explain [:andn [:m :map] [:v [:vector :any]]] {})))) + (is (= #malli.core.Tags{:values {:m {} :f {}}} + (m/parse [:andn [:m :map] [:f [:fn map?]]] {}))) + (let [s [:andn [:m :map] [:f [:fn map?]]]] + (is (= {} (->> {} (m/parse s) (m/unparse s))))) + (is (= #malli.core.Tags{:values {:o #malli.core.Tag{:key :left, :value 1}, :f 1}} + (m/parse [:andn [:o [:orn [:left :int] [:right :int]]] [:f [:fn number?]]] 1))) + (let [s [:andn [:o [:orn [:left :int] [:right :int]]] [:f [:fn number?]]]] + (is (= 1 (->> 1 (m/parse s) (m/unparse s))))) + (let [s [:andn [:o [:orn [:left :int] [:right :int]]] [:f [:fn number?]]] + p (m/parse s 1) + _ (is (= #malli.core.Tags{:values {:o #malli.core.Tag{:key :left, :value 1}, :f 1}} p))] + (testing "left-most parse is used" + (is (= 2 (m/unparse s (update-in p [:values :o :value] inc)))) + (is (= 1 (m/unparse s (update-in p [:values :f] inc)))) + (is (= 2 (m/unparse s (-> p + (update-in [:values :f] inc) + (update :values dissoc :o))))) + (is (= ::m/invalid (m/unparse s (update p :values dissoc :o :f))))))) From 9ef590c14d7b9d77033331f04479e7a0c44651b9 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 06:26:14 +0000 Subject: [PATCH 02/22] doc --- README.md | 45 +++++++++++++++++++++++++++++++++------ test/malli/core_test.cljc | 5 ++++- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index ea7309ada..7274f0278 100644 --- a/README.md +++ b/README.md @@ -2511,26 +2511,57 @@ Parsing returns tagged values for `:orn`, `:catn`, `:altn`, `:andn` and `:multi` ### Parsing `:and` The `:and` schema is unusual in that it parses multiple schemas and yet only returns the results of parsing one of them. +Which schema is used for parsing is usually chosen automatically. -This works seamlessly, unless more than one conjunct's parser transforms its input. -Examples of schemas that can transform their input are `:orn` (returns `Tag`), -`:catn` (returns `Tags`), and any composite schema such as `:map` (recursively transforms children). +```clojure +(m/parse [:and [:orn [:left :int] [:right :int]] [:fn number?]] 1) +; => #malli.core.Tag{:key :left, :value 1} +(m/parse [:and [:fn number?] [:orn [:left :int] [:right :int]]] 1) +; => #malli.core.Tag{:key :left, :value 1} +``` The error `:malli.core/and-schema-multiple-transforming-parsers` is thrown if the transforming -parser cannot be picked automatically. There are several ways to resolve this. +parser cannot be picked automatically. This usually means that multiple conjuncts +will transform their input or a false-positive has occurred because the underlying schema +does not implement `malli.core/ParserInfo`. + +```clojure +(m/parser [:and [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]) +; Execution error (ExceptionInfo) at malli.core/-exception (core.cljc:189). +; :malli.core/and-schema-multiple-transforming-parsers +``` + +There are several ways to resolve this. If you know a single conjunct should exclusively parse the input, use the `:parse` property to identify the conjunct by index. +To opt-out of parsing any further levels of this schema, use the `:parse :none` property. -[:and [:and [:orn [:l :int] [:r :boolean]]]] -[:and [:and [:orn [:l :int] [:r :boolean]]]] +```clojure +(m/parse [:and {:parse 0} [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]) +; => {:left #malli.core.Tag{:key :one, :value 1}, :right 1} -To opt-out of parsing any further levels of this schema, use the `:parse :none` property. +(m/parse [:and {:parse 1} [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]) +; => {:left 1, :right #malli.core.Tag{:key :one, :value 1}} + +(m/parse [:and {:parse :none} [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]) +; => {:left 1, :right 1} +``` To parse all conjuncts, you must migrate the schema to `:andn`. This involves tagging each conjunct with syntax like `:orn` and `:map`. The results of parsing will be wrapped in `Tags`. Only the left-most child will be unparsed, useful if you plan to modify the results of parsing. +```clojure +(m/parse [:andn [:l [:map [:left [:orn [:one :int]]]]] [:r [:map [:right [:orn [:one :int]]]]]] {:left 1 :right 1}) +; => #malli.core.Tags{:values {:l {:left #malli.core.Tag{:key :one, :value 1}, :right 1}, + :r {:left 1, :right #malli.core.Tag{:key :one, :value 1}}}} + +(m/unparse [:andn [:l [:map [:left [:orn [:one :int]]]]] [:r [:map [:right [:orn [:one :int]]]]]] + #malli.core.Tags{:values {:r {:left 3, :right #malli.core.Tag{:key :one, :value 1}}}}) +; => {:left 3, :right 1} +``` + ## Unparsing values The inverse of parsing, using `m/unparse` and `m/unparser`: diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 1bed7dbc8..d5b35b252 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3634,7 +3634,10 @@ (m/parser [:and [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]))) (is (-> (m/schema [:vector :int]) m/-parser-info :simple-parser)) (is (-> (m/schema [:vector [:orn [:one :int]]]) m/-parser-info :simple-parser not))) - +(comment + (m/unparse [:andn [:l [:map [:left [:orn [:one :int]]]]] [:r [:map [:right [:orn [:one :int]]]]]] + #malli.core.Tags{:values {;:l {:left #malli.core.Tag{:key :one, :value 1}, :right 1}, + :r {:left 3, :right #malli.core.Tag{:key :one, :value 1}}}})) (deftest andn-test (is (= {:schema [:andn [:m :map] [:v [:vector :any]]], :value {}, From 20036a16937aca40a0d1c123cb1b6b5d776ffbae Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 06:26:26 +0000 Subject: [PATCH 03/22] wip --- test/malli/core_test.cljc | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index d5b35b252..1bed7dbc8 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3634,10 +3634,7 @@ (m/parser [:and [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]))) (is (-> (m/schema [:vector :int]) m/-parser-info :simple-parser)) (is (-> (m/schema [:vector [:orn [:one :int]]]) m/-parser-info :simple-parser not))) -(comment - (m/unparse [:andn [:l [:map [:left [:orn [:one :int]]]]] [:r [:map [:right [:orn [:one :int]]]]]] - #malli.core.Tags{:values {;:l {:left #malli.core.Tag{:key :one, :value 1}, :right 1}, - :r {:left 3, :right #malli.core.Tag{:key :one, :value 1}}}})) + (deftest andn-test (is (= {:schema [:andn [:m :map] [:v [:vector :any]]], :value {}, From 17757504715ea0b46659e91edfce556df600c963 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 13:57:05 +0000 Subject: [PATCH 04/22] wip --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7274f0278..f8991ba96 100644 --- a/README.md +++ b/README.md @@ -2558,7 +2558,7 @@ Only the left-most child will be unparsed, useful if you plan to modify the resu :r {:left 1, :right #malli.core.Tag{:key :one, :value 1}}}} (m/unparse [:andn [:l [:map [:left [:orn [:one :int]]]]] [:r [:map [:right [:orn [:one :int]]]]]] - #malli.core.Tags{:values {:r {:left 3, :right #malli.core.Tag{:key :one, :value 1}}}}) + (m/tags {:r {:left 3, :right (m/tag :one 1)}})) ; => {:left 3, :right 1} ``` @@ -2577,7 +2577,7 @@ The inverse of parsing, using `m/unparse` and `m/unparser`: ```clojure (m/unparse [:orn [:name :string] [:id :int]] - (m/tagged :name "x")) + (m/tag :name "x")) ; => "x" (m/unparse [:* [:catn [:name :string] [:id :int]]] From 9cde5b1163d127ed78e00904c413b73b68f71c40 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 14:27:29 +0000 Subject: [PATCH 05/22] wip --- README.md | 30 ++++++++++++++++++++++++++---- test/malli/core_test.cljc | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f8991ba96..75bdc18fa 100644 --- a/README.md +++ b/README.md @@ -2510,7 +2510,7 @@ Parsing returns tagged values for `:orn`, `:catn`, `:altn`, `:andn` and `:multi` ### Parsing `:and` -The `:and` schema is unusual in that it parses multiple schemas and yet only returns the results of parsing one of them. +The `:and` schema combines multiple schemas for the same value and yet only returns the results of parsing one of them. Which schema is used for parsing is usually chosen automatically. ```clojure @@ -2575,15 +2575,37 @@ The inverse of parsing, using `m/unparse` and `m/unparser`: ; [:p "Hello, world of data"]] ``` +Tags are mapped back to the original schema to be unparsed. + ```clojure -(m/unparse [:orn [:name :string] [:id :int]] - (m/tag :name "x")) +(m/unparse [:orn [:left :string] [:right :int]] + (m/tag :left "x")) ; => "x" -(m/unparse [:* [:catn [:name :string] [:id :int]]] +(m/unparse [:orn [:left :string] [:right :int]] + (m/tag :left 1)) +; => ::m/invalid +``` + +Unparsing can be used to update complex values via an associative interface. + +```clojure +(def FlatPairs [:* [:catn [:name :string] [:id :int]]]) + +(m/parse FlatPairs ["x" 1 "y" 2]) +; => [#malli.core.Tags{:values {:name "x", :id 1}} +; #malli.core.Tags{:values {:name "y", :id 2}}] + +(m/unparse FlatPairs [(m/tags {:name "x" :id 1}) (m/tags {:name "y" :id 2})]) ; => ["x" 1 "y" 2] + +(->> ["x" 1 "y" 2 "z" 3] + (m/parse FlatPairs) + (mapv (fn [tags] (-> tags (update :values #(-> % (update :name str "_") (update :id * 2)))))) + (m/unparse FlatPairs)) +; => ["x_" 2 "y_" 4 "z_" 6] ``` ## Serializable functions diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 1bed7dbc8..6fd1768ff 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3665,3 +3665,38 @@ (update-in [:values :f] inc) (update :values dissoc :o))))) (is (= ::m/invalid (m/unparse s (update p :values dissoc :o :f))))))) + +(comment +(def Hiccup + [:schema {:registry {"hiccup" [:orn + [:node [:catn + [:name keyword?] + [:props [:? [:map-of keyword? any?]]] + [:children [:* [:schema [:ref "hiccup"]]]]]] + [:primitive [:orn + [:nil nil?] + [:boolean boolean?] + [:number number?] + [:text string?]]]]}} + "hiccup"]) + +(def parse-hiccup (m/parser Hiccup)) + +(pr-str (parse-hiccup + [:div {:class [:foo :bar]} + [:p "Hello, world of data"]])) + +(def FlatPairs [:* [:catn [:name :string] [:id :int]]]) +(m/unparse [:orn [:name :string] [:id :int]] + (m/tag :name "x")) +(->> ["x" 1 "y" 2 "z" 3] + (m/parse FlatPairs) + (mapv (fn [tags] (-> tags (update :values #(-> % (update :name str "_") (update :id * 2)))))) + (m/unparse FlatPairs)) +(m/unparse [:orn [:left [:* :string]] [:right :string]] + (m/tag :left "x")) +(m/unparse [:orn [:left :string] [:right :int]] + (m/tag :left 1)) +(pr-str (m/parse FlatPairs ["x" 1 "y" 2])) + ) + From 0cd88fe56990ed620a13d0a889231bf7b95e184b Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 14:51:45 +0000 Subject: [PATCH 06/22] wip --- README.md | 34 ++++++++++++++++++++++++++-------- test/malli/core_test.cljc | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 75bdc18fa..4e9da83f0 100644 --- a/README.md +++ b/README.md @@ -2549,17 +2549,35 @@ To opt-out of parsing any further levels of this schema, use the `:parse :none` ``` To parse all conjuncts, you must migrate the schema to `:andn`. This involves tagging each conjunct -with syntax like `:orn` and `:map`. The results of parsing will be wrapped in `Tags`. +with syntax like `:orn` and `:map`. The results of parsing `:andn` will be wrapped in `Tags` with +an entry for parsing the original value with each conjunct. + Only the left-most child will be unparsed, useful if you plan to modify the results of parsing. ```clojure -(m/parse [:andn [:l [:map [:left [:orn [:one :int]]]]] [:r [:map [:right [:orn [:one :int]]]]]] {:left 1 :right 1}) -; => #malli.core.Tags{:values {:l {:left #malli.core.Tag{:key :one, :value 1}, :right 1}, - :r {:left 1, :right #malli.core.Tag{:key :one, :value 1}}}} - -(m/unparse [:andn [:l [:map [:left [:orn [:one :int]]]]] [:r [:map [:right [:orn [:one :int]]]]]] - (m/tags {:r {:left 3, :right (m/tag :one 1)}})) -; => {:left 3, :right 1} +(def Paired+Flat + [:andn + [:paired [:* [:catn [:name :string] [:id :int]]]] + [:flat [:vector [:orn [:name :string] [:id :int]]]]]) + +(m/parse Paired+Flat ["x" 1 "y" 2]) +; => #malli.core.Tags{:values +; {:paired [#malli.core.Tags{:values {:name "x", :id 1}} +; #malli.core.Tags{:values {:name "y", :id 2}}], +; :flat [#malli.core.Tag{:key :name, :value "x"} +; #malli.core.Tag{:key :id, :value 1} +; #malli.core.Tag{:key :name, :value "y"} +; #malli.core.Tag{:key :id, :value 2}]}} + +(as-> ["x" 1 "y" 2] $ + (m/parse Paired+Flat $) + (update $ :values + (fn [{:keys [flat paired] :as res}] + ;; remove other :andn results like :flat when transforming + {:paired (map-indexed (fn [i p] (update-in p [:values :id] * (+ 2 i) (count flat))) + (rseq paired))})) + (m/unparse Paired+Flat $)) +["y" 16 "x" 12] ``` ## Unparsing values diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 6fd1768ff..2e8e2bd1c 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3698,5 +3698,37 @@ (m/unparse [:orn [:left :string] [:right :int]] (m/tag :left 1)) (pr-str (m/parse FlatPairs ["x" 1 "y" 2])) + +(def Paired+Flat + [:andn + [:paired [:* [:catn [:name :string] [:id :int]]]] + [:flat [:vector [:orn [:name :string] [:id :int]]]]]) + +(m/parse Paired+Flat ["x" 1 "y" 2]) +; => #malli.core.Tags{:values +; {:paired [#malli.core.Tags{:values {:name "x", :id 1}} +; #malli.core.Tags{:values {:name "y", :id 2}}], +; :flat [#malli.core.Tag{:key :name, :value "x"} +; #malli.core.Tag{:key :id, :value 1} +; #malli.core.Tag{:key :name, :value "y"} +; #malli.core.Tag{:key :id, :value 2}]}} + +(as-> ["x" 1 "y" 2] $ + (m/parse Paired+Flat $) + (update $ :values + (fn [{:keys [flat paired] :as res}] + ;; remove other :andn results like :flat when transforming + {:paired (rseq (mapv #(update-in % [:values :id] + (count flat)) paired))})) + (m/unparse Paired+Flat $)) +; => ["y" 6 "x" 5] +(as-> ["x" 1 "y" 2] $ + (m/parse Paired+Flat $) + (update $ :values + (fn [{:keys [flat paired] :as res}] + ;; remove other :andn results like :flat when transforming + {:paired (map-indexed (fn [i p] (update-in p [:values :id] * (+ 2 i) (count flat))) + (rseq paired))})) + (m/unparse Paired+Flat $)) +["y" 16 "x" 12] ) From b471e5ef2960d5919744608196fbf40b6df987bb Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 14:52:15 +0000 Subject: [PATCH 07/22] rm --- test/malli/core_test.cljc | 67 --------------------------------------- 1 file changed, 67 deletions(-) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 2e8e2bd1c..1bed7dbc8 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3665,70 +3665,3 @@ (update-in [:values :f] inc) (update :values dissoc :o))))) (is (= ::m/invalid (m/unparse s (update p :values dissoc :o :f))))))) - -(comment -(def Hiccup - [:schema {:registry {"hiccup" [:orn - [:node [:catn - [:name keyword?] - [:props [:? [:map-of keyword? any?]]] - [:children [:* [:schema [:ref "hiccup"]]]]]] - [:primitive [:orn - [:nil nil?] - [:boolean boolean?] - [:number number?] - [:text string?]]]]}} - "hiccup"]) - -(def parse-hiccup (m/parser Hiccup)) - -(pr-str (parse-hiccup - [:div {:class [:foo :bar]} - [:p "Hello, world of data"]])) - -(def FlatPairs [:* [:catn [:name :string] [:id :int]]]) -(m/unparse [:orn [:name :string] [:id :int]] - (m/tag :name "x")) -(->> ["x" 1 "y" 2 "z" 3] - (m/parse FlatPairs) - (mapv (fn [tags] (-> tags (update :values #(-> % (update :name str "_") (update :id * 2)))))) - (m/unparse FlatPairs)) -(m/unparse [:orn [:left [:* :string]] [:right :string]] - (m/tag :left "x")) -(m/unparse [:orn [:left :string] [:right :int]] - (m/tag :left 1)) -(pr-str (m/parse FlatPairs ["x" 1 "y" 2])) - -(def Paired+Flat - [:andn - [:paired [:* [:catn [:name :string] [:id :int]]]] - [:flat [:vector [:orn [:name :string] [:id :int]]]]]) - -(m/parse Paired+Flat ["x" 1 "y" 2]) -; => #malli.core.Tags{:values -; {:paired [#malli.core.Tags{:values {:name "x", :id 1}} -; #malli.core.Tags{:values {:name "y", :id 2}}], -; :flat [#malli.core.Tag{:key :name, :value "x"} -; #malli.core.Tag{:key :id, :value 1} -; #malli.core.Tag{:key :name, :value "y"} -; #malli.core.Tag{:key :id, :value 2}]}} - -(as-> ["x" 1 "y" 2] $ - (m/parse Paired+Flat $) - (update $ :values - (fn [{:keys [flat paired] :as res}] - ;; remove other :andn results like :flat when transforming - {:paired (rseq (mapv #(update-in % [:values :id] + (count flat)) paired))})) - (m/unparse Paired+Flat $)) -; => ["y" 6 "x" 5] -(as-> ["x" 1 "y" 2] $ - (m/parse Paired+Flat $) - (update $ :values - (fn [{:keys [flat paired] :as res}] - ;; remove other :andn results like :flat when transforming - {:paired (map-indexed (fn [i p] (update-in p [:values :id] * (+ 2 i) (count flat))) - (rseq paired))})) - (m/unparse Paired+Flat $)) -["y" 16 "x" 12] - ) - From 964d1cccb1ce6a72e18dc626a124eef71220cbf7 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 20:16:43 +0000 Subject: [PATCH 08/22] add :andn --- src/malli/clj_kondo.cljc | 1 + src/malli/core.cljc | 19 +++++++++---------- src/malli/experimental/describe.cljc | 1 + src/malli/generator.cljc | 1 + src/malli/json_schema.cljc | 1 + src/malli/swagger.cljc | 10 ++++++++++ 6 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/malli/clj_kondo.cljc b/src/malli/clj_kondo.cljc index 4d4190cbc..dd6c3e7e5 100644 --- a/src/malli/clj_kondo.cljc +++ b/src/malli/clj_kondo.cljc @@ -67,6 +67,7 @@ (defmethod accept :not= [_ _ _ _] :any) ;;?? (defmethod accept :and [_ _ _ _] :any) ;;?? +(defmethod accept :andn [_ _ _ _] :any) ;;?? (defmethod accept :or [_ _ _ _] :any) ;;?? (defmethod accept :orn [_ _ _ _] :any) ;;?? (defmethod accept :not [_ _ _ _] :any) ;;?? diff --git a/src/malli/core.cljc b/src/malli/core.cljc index e67115ce1..0888051a9 100644 --- a/src/malli/core.cljc +++ b/src/malli/core.cljc @@ -896,15 +896,15 @@ (-parser [this] (let [k+parsers (-vmap (fn [[k _ c]] [k (-parser c)]) (-children this))] (fn [x] - (let [tags (reduce (fn [acc [k parser]] - (let [x' (parser x)] - (if (miu/-invalid? x') - (reduced ::invalid) - (assoc acc k x')))) - {} k+parsers)] - (if (miu/-invalid? tags) + (let [values (reduce (fn [acc [k parser]] + (let [x' (parser x)] + (if (miu/-invalid? x') + (reduced ::invalid) + (assoc acc k x')))) + {} k+parsers)] + (if (miu/-invalid? values) ::invalid - (->Tags tags)))))) + (->Tags values)))))) (-unparser [this] ;; only the left-most child provided in tags is unparsed. the remaining values are ignored. ;; the unparsed value is checked against the remaining children. @@ -925,8 +925,7 @@ ::invalid) ::invalid)))) (-transformer [this transformer method options] - ;FIXME !!! - (-or-transformer this transformer (-vmap #(nth % 2) (-children this)) method options)) + (-parent-children-transformer this (-vmap #(nth % 2) (-children this)) transformer method options)) (-walk [this walker path options] (-walk-entries this walker path options)) (-properties [_] properties) (-options [_] options) diff --git a/src/malli/experimental/describe.cljc b/src/malli/experimental/describe.cljc index 8caa77466..b8db7bd3d 100644 --- a/src/malli/experimental/describe.cljc +++ b/src/malli/experimental/describe.cljc @@ -158,6 +158,7 @@ (defmethod accept :select-keys [_ schema _ {::keys [describe] :as options}] (describe (m/deref schema) options)) (defmethod accept :and [_ s children _] (str (str/join ", and " children) (-titled s))) +(defmethod accept :andn [_ s children _] (str (str/join ", and " (-tagged children)) (-titled s))) (defmethod accept :enum [_ s children _options] (str "enum" (-titled s) " of " (str/join ", " children))) (defmethod accept :maybe [_ s children _] (str "nullable " (-titled s) (first children))) (defmethod accept :tuple [_ s children _] (str "vector " (-titled s) "with exactly " (count children) " items of type: " (str/join ", " children))) diff --git a/src/malli/generator.cljc b/src/malli/generator.cljc index 407890bd4..cf6028dfa 100644 --- a/src/malli/generator.cljc +++ b/src/malli/generator.cljc @@ -383,6 +383,7 @@ (defmethod -schema-generator 'neg? [_ options] (gen/one-of [(gen-double {:max -0.00001}) (gen-fmap (comp dec -) gen/nat)])) (defmethod -schema-generator :not [schema options] (gen-such-that schema (m/validator schema options) (ga/gen-for-pred any?))) (defmethod -schema-generator :and [schema options] (-and-gen schema options)) +(defmethod -schema-generator :andn [schema options] (-and-gen (m/into-schema :and (m/properties schema) (map last (m/children schema)) (m/options schema)) options)) (defmethod -schema-generator :or [schema options] (-or-gen schema options)) (defmethod -schema-generator :orn [schema options] (-or-gen (m/into-schema :or (m/properties schema) (map last (m/children schema)) (m/options schema)) options)) (defmethod -schema-generator ::m/val [schema options] (-child-gen schema options)) diff --git a/src/malli/json_schema.cljc b/src/malli/json_schema.cljc index fc64fa115..f1a0682f4 100644 --- a/src/malli/json_schema.cljc +++ b/src/malli/json_schema.cljc @@ -92,6 +92,7 @@ (defmethod accept :not [_ _ children _] {:not (last children)}) (defmethod accept :and [_ _ children _] {:allOf children}) +(defmethod accept :andn [_ _ children _] {:allOf (map last children)}) (defmethod accept :or [_ _ children _] {:anyOf children}) (defmethod accept :orn [_ _ children _] {:anyOf (map last children)}) diff --git a/src/malli/swagger.cljc b/src/malli/swagger.cljc index c490c1a56..d5e108671 100644 --- a/src/malli/swagger.cljc +++ b/src/malli/swagger.cljc @@ -24,10 +24,20 @@ (let [base (-base s children)] (assoc base :x-allOf children))) +(defmethod accept :andn [_ s children _] + (let [children (map last children) + base (-base s children)] + (assoc base :x-allOf children))) + (defmethod accept :or [_ s children _] (let [base (-base s children)] (assoc base :x-anyOf children))) +(defmethod accept :orn [_ s children _] + (let [children (map last children) + base (-base s children)] + (assoc base :x-anyOf children))) + (defmethod accept :multi [_ s children _] (let [cs (mapv last children) base (-base s cs)] From 34b4409c849323206cac228ffc194dd49f01cdce Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 20:22:26 +0000 Subject: [PATCH 09/22] move --- src/malli/experimental/describe.cljc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/malli/experimental/describe.cljc b/src/malli/experimental/describe.cljc index b8db7bd3d..03d310dfc 100644 --- a/src/malli/experimental/describe.cljc +++ b/src/malli/experimental/describe.cljc @@ -157,6 +157,8 @@ (defmethod accept :union [_ schema _ {::keys [describe] :as options}] (describe (m/deref schema) options)) (defmethod accept :select-keys [_ schema _ {::keys [describe] :as options}] (describe (m/deref schema) options)) +(defn -tagged [children] (map (fn [[tag _ c]] (str c " (tag: " tag ")")) children)) + (defmethod accept :and [_ s children _] (str (str/join ", and " children) (-titled s))) (defmethod accept :andn [_ s children _] (str (str/join ", and " (-tagged children)) (-titled s))) (defmethod accept :enum [_ s children _options] (str "enum" (-titled s) " of " (str/join ", " children))) @@ -201,8 +203,6 @@ (defmethod accept :function [_ _ _children _] "function") (defmethod accept :fn [_ _ _ _] "function") -(defn -tagged [children] (map (fn [[tag _ c]] (str c " (tag: " tag ")")) children)) - (defmethod accept :or [_ _ children _] (str/join ", or " children)) (defmethod accept :orn [_ _ children _] (str/join ", or " (-tagged children))) From 09cbe2f9cc2e79c9a600416671b917254a688380 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 20:23:33 +0000 Subject: [PATCH 10/22] wip --- src/malli/experimental/describe.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/malli/experimental/describe.cljc b/src/malli/experimental/describe.cljc index 03d310dfc..0ead21cfb 100644 --- a/src/malli/experimental/describe.cljc +++ b/src/malli/experimental/describe.cljc @@ -253,7 +253,7 @@ ;; (defn describe - "Given a schema, returns a string explaiaing the required shape in English" + "Given a schema, returns a string explaining the required shape in English" ([?schema] (describe ?schema nil)) ([?schema options] From 480ce51e02e6acbb4b4445162fe22553b2027980 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 21:59:16 +0000 Subject: [PATCH 11/22] add test --- src/malli/core.cljc | 11 +++-- test/malli/core_test.cljc | 89 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/src/malli/core.cljc b/src/malli/core.cljc index 0888051a9..d8b0b31fa 100644 --- a/src/malli/core.cljc +++ b/src/malli/core.cljc @@ -867,7 +867,9 @@ (-get [_ key default] (get children key default)) (-set [this key value] (-set-assoc-children this key value)) ParserInfo - (-parser-info [_] (if-some [i @transforming-parser] (-parser-info (nth children i)) {}))))))) + (-parser-info [_] (if-some [i @transforming-parser] + (-parser-info (nth children i)) + {:simple-parser true}))))))) (defn -andn-schema [] ^{:type ::into-schema} @@ -1283,7 +1285,7 @@ (-get [this key default] (-get-entries this key default)) (-set [this key value] (-set-entries this key value)) ParserInfo - (-parser-info [_] {:simple-parser (every? (comp :simple-parser -parser-info) (-entry-children entry-parser))}))))))) + (-parser-info [_] {:simple-parser (every? (comp :simple-parser -parser-info peek) (-entry-children entry-parser))}))))))) (defn -map-of-schema ([] @@ -1504,7 +1506,8 @@ (-get [_ _ _] schema) (-set [this _ value] (-set-children this [value])) ParserInfo - (-parser-info [_] (-parser-info schema)))))))))) + (-parser-info [_] (cond-> (-parser-info schema) + bounded (assoc :simple-parser true))))))))))) (defn -tuple-schema ([] @@ -1578,7 +1581,7 @@ (-get [_ key default] (get children key default)) (-set [this key value] (-set-assoc-children this key value)) ParserInfo - (-parser-info [_] (every? (comp :simple-parser -parser-info) children)))))))) + (-parser-info [_] {:simple-parser (every? (comp :simple-parser -parser-info) children)}))))))) (defn -enum-schema [] ^{:type ::into-schema} diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 1bed7dbc8..1a9347298 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3585,6 +3585,61 @@ (is (not (m/validate [:seqable {:min 11} :int] (eduction identity (range 10))))) (is (nil? (m/explain [:sequential {:min 9} :int] (eduction identity (range 10)))))) +(defn simple-parser? [s] (boolean (:simple-parser (m/-parser-info (m/schema s))))) + +(def simple-parser-templates + "Schemas which have simple parsers iff ::HOLE has a simple parser. + Should also be generatable for any ::HOLE." + [::HOLE + [:schema ::HOLE] + [:schema {:registry {::a ::HOLE}} ::a] + [:schema {:registry {::a ::HOLE}} [:ref ::a]] + [:tuple ::HOLE] + [:tuple ::HOLE :any] + [:vector ::HOLE] + [:map [:foo ::HOLE]] + [:map [:foo ::HOLE] [:bar :int]] + [:and ::HOLE] ;; generator will fail if :any is first + [:and ::HOLE :any]]) + +(def simple-parser-schemas [:any [:and :any] :int map? :map :tuple [:seqable :any] [:every :catn]]) +(def transforming-parser-schemas [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch any?} [true :any]]]) + +(deftest parser-info-test + (is (simple-parser? :any)) + (is (not (simple-parser? :catn))) + (let [d (m/default-schemas)] + (doseq [[hole expected] (concat (map vector simple-parser-schemas (repeat true)) + (map vector transforming-parser-schemas (repeat false))) + :let [_ (testing (pr-str hole) + (is (= expected (simple-parser? hole))))] + template simple-parser-templates + :let [s (testing {:template template :hole hole} + (is (m/schema template {:registry (assoc d ::HOLE (m/schema hole {:registry d}))})))]] + (testing (pr-str {:s (m/form s) :hole hole}) + (is (= expected (simple-parser? s))) + (let [parse (m/parser s) + unparse (m/parser s)] + (if expected + (doseq [g (is (doall (mg/sample s)))] + (testing (pr-str g) + (let [p (parse g)] + (is (identical? g p)) + (is (identical? g (unparse p)))))) + (is (some (fn [g] + (let [p (parse g)] + (and (not (identical? g p)) + (not (identical? g (unparse p)))))) + (mapcat #(mg/sample s {:size %}) [10 100 1000])))))))) + (is (simple-parser? [:schema :any])) + (is (not (simple-parser? [:schema :catn]))) + (is (simple-parser? [:schema {:registry {::a :any}} ::a])) + (is (simple-parser? [:schema {:registry {::a :any}} ::a])) + (is (simple-parser? [:schema {:registry {::a :any}} [:ref ::a]])) + (is (every? simple-parser? [[:tuple] [:tuple :any] [:tuple :any :any]])) + (is (not (simple-parser? [:tuple [:catn]]))) +) + (deftest and-complex-parser-test (is (= {} (m/parse [:and :map [:fn map?]] {}))) (is (= {} (m/parse [:and [:fn map?] :map] {}))) @@ -3633,7 +3688,39 @@ #":malli\.core/and-schema-multiple-transforming-parsers" (m/parser [:and [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]))) (is (-> (m/schema [:vector :int]) m/-parser-info :simple-parser)) - (is (-> (m/schema [:vector [:orn [:one :int]]]) m/-parser-info :simple-parser not))) + (is (-> (m/schema [:vector [:orn [:one :int]]]) m/-parser-info :simple-parser not)) + (is (= #malli.core.Tags{:values {"a" 3, "b" :x}} + (m/parse [:and [:catn ["a" :int] ["b" :keyword]] + [:fn vector?]] + [3 :x]))) + (let [s [:and [:catn ["a" :int] ["b" :keyword]] + [:fn vector?]] + res (->> [3 :x] + (m/parse s) + (m/unparse s))] + (is (= [3 :x] res)) + (is (m/validate s res))) + (let [s [:and [:catn ["a" :int] ["b" :keyword]] + [:vector :any]] + res (->> [3 :x] + (m/parse s) + (m/unparse s))] + (is (= [3 :x] res)) + (is (m/validate s res))) + (let [s [:and [:catn ["a" :int] ["b" :keyword]] + [:sequential :any]] + res (->> [3 :x] + (m/parse s) + (m/unparse s))] + (is (= [3 :x] res)) + (is (m/validate s res))) + (let [s [:and [:catn ["a" :int] ["b" :keyword]] + [:tuple :any :any]] + res (->> [3 :x] + (m/parse s) + (m/unparse s))] + (is (= [3 :x] res)) + (is (m/validate s res)))) (deftest andn-test (is (= {:schema [:andn [:m :map] [:v [:vector :any]]], From 4155dd92a427e18952361a1509b54e6f97816ff4 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 22:15:23 +0000 Subject: [PATCH 12/22] wip --- test/malli/core_test.cljc | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 1a9347298..1052b73fa 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3600,10 +3600,18 @@ [:map [:foo ::HOLE]] [:map [:foo ::HOLE] [:bar :int]] [:and ::HOLE] ;; generator will fail if :any is first - [:and ::HOLE :any]]) + [:and ::HOLE :any] + [:and ::HOLE :any :any] + [:or ::HOLE] ;; parser will always be identical if :any is first + [:or ::HOLE :any]]) -(def simple-parser-schemas [:any [:and :any] :int map? :map :tuple [:seqable :any] [:every :catn]]) -(def transforming-parser-schemas [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch any?} [true :any]]]) +(def simple-parser-schemas [:any [:and :any] :int #'map? :map :tuple [:seqable :any] [:every :catn]]) +(def transforming-parser-schemas [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]]]) + +(comment + (mg/sample (m/schema [:or :any :malli.core-test/HOLE] {:registry (assoc (m/default-schemas) :malli.core-test/HOLE (m/schema [:multi {:dispatch #'any?} [true :any]]))}) + {:size 100}) + ) (deftest parser-info-test (is (simple-parser? :any)) @@ -3615,8 +3623,10 @@ (is (= expected (simple-parser? hole))))] template simple-parser-templates :let [s (testing {:template template :hole hole} - (is (m/schema template {:registry (assoc d ::HOLE (m/schema hole {:registry d}))})))]] - (testing (pr-str {:s (m/form s) :hole hole}) + (is (m/schema template {:registry (assoc d ::HOLE (m/schema hole))})))]] + (testing (pr-str (list 'm/schema template + {:registry (list 'assoc (list 'm/default-schemas) + (symbol "::HOLE") (list 'm/schema hole))})) (is (= expected (simple-parser? s))) (let [parse (m/parser s) unparse (m/parser s)] @@ -3630,7 +3640,7 @@ (let [p (parse g)] (and (not (identical? g p)) (not (identical? g (unparse p)))))) - (mapcat #(mg/sample s {:size %}) [10 100 1000])))))))) + (mg/sample s {:size 3 :seed 0})))))))) (is (simple-parser? [:schema :any])) (is (not (simple-parser? [:schema :catn]))) (is (simple-parser? [:schema {:registry {::a :any}} ::a])) From 9cde29bc8bc0013293e4e867788850b886709975 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 22:17:53 +0000 Subject: [PATCH 13/22] wip --- test/malli/core_test.cljc | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 1052b73fa..b052e561b 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3589,7 +3589,8 @@ (def simple-parser-templates "Schemas which have simple parsers iff ::HOLE has a simple parser. - Should also be generatable for any ::HOLE." + Should also be generatable for any ::HOLE and be capable to parsing + to a non-identical value than its input." [::HOLE [:schema ::HOLE] [:schema {:registry {::a ::HOLE}} ::a] @@ -3608,14 +3609,7 @@ (def simple-parser-schemas [:any [:and :any] :int #'map? :map :tuple [:seqable :any] [:every :catn]]) (def transforming-parser-schemas [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]]]) -(comment - (mg/sample (m/schema [:or :any :malli.core-test/HOLE] {:registry (assoc (m/default-schemas) :malli.core-test/HOLE (m/schema [:multi {:dispatch #'any?} [true :any]]))}) - {:size 100}) - ) - (deftest parser-info-test - (is (simple-parser? :any)) - (is (not (simple-parser? :catn))) (let [d (m/default-schemas)] (doseq [[hole expected] (concat (map vector simple-parser-schemas (repeat true)) (map vector transforming-parser-schemas (repeat false))) @@ -3640,15 +3634,7 @@ (let [p (parse g)] (and (not (identical? g p)) (not (identical? g (unparse p)))))) - (mg/sample s {:size 3 :seed 0})))))))) - (is (simple-parser? [:schema :any])) - (is (not (simple-parser? [:schema :catn]))) - (is (simple-parser? [:schema {:registry {::a :any}} ::a])) - (is (simple-parser? [:schema {:registry {::a :any}} ::a])) - (is (simple-parser? [:schema {:registry {::a :any}} [:ref ::a]])) - (is (every? simple-parser? [[:tuple] [:tuple :any] [:tuple :any :any]])) - (is (not (simple-parser? [:tuple [:catn]]))) -) + (mg/sample s {:size 3 :seed 0}))))))))) (deftest and-complex-parser-test (is (= {} (m/parse [:and :map [:fn map?]] {}))) From fa4650399d7047b21ee2537ba46df818151d5bd0 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 22:23:12 +0000 Subject: [PATCH 14/22] wip --- test/malli/core_test.cljc | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index b052e561b..d99b069c5 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3589,12 +3589,13 @@ (def simple-parser-templates "Schemas which have simple parsers iff ::HOLE has a simple parser. - Should also be generatable for any ::HOLE and be capable to parsing - to a non-identical value than its input." + Should also be generatable for any ::HOLE and be capable to (un)parsing + to a different value than its input if transforming." [::HOLE [:schema ::HOLE] [:schema {:registry {::a ::HOLE}} ::a] [:schema {:registry {::a ::HOLE}} [:ref ::a]] + [:schema {:registry {::a [:ref ::b] ::b ::HOLE}} [:ref ::a]] [:tuple ::HOLE] [:tuple ::HOLE :any] [:vector ::HOLE] @@ -3604,10 +3605,17 @@ [:and ::HOLE :any] [:and ::HOLE :any :any] [:or ::HOLE] ;; parser will always be identical if :any is first - [:or ::HOLE :any]]) - -(def simple-parser-schemas [:any [:and :any] :int #'map? :map :tuple [:seqable :any] [:every :catn]]) -(def transforming-parser-schemas [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]]]) + [:or ::HOLE :any] + [:map-of ::HOLE :any] + [:map-of :any ::HOLE] + [:map-of ::HOLE ::HOLE] + ]) + +(def simple-parser-schemas [:any [:and :any] :int #'map? :map :tuple [:seqable :any] [:every :catn] + [:fn {:gen/schema :any} #'any?] + [:map-of :any :any]]) +(def transforming-parser-schemas [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]] + #_[:map [:any :any]]]) (deftest parser-info-test (let [d (m/default-schemas)] @@ -3634,7 +3642,7 @@ (let [p (parse g)] (and (not (identical? g p)) (not (identical? g (unparse p)))))) - (mg/sample s {:size 3 :seed 0}))))))))) + (mg/sample s {:seed 0}))))))))) (deftest and-complex-parser-test (is (= {} (m/parse [:and :map [:fn map?]] {}))) From 28cb8e1d1686e925620decf3a6b00cd0d3c1b457 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 22:41:28 +0000 Subject: [PATCH 15/22] wip --- test/malli/core_test.cljc | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index d99b069c5..d03590f6f 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3592,6 +3592,7 @@ Should also be generatable for any ::HOLE and be capable to (un)parsing to a different value than its input if transforming." [::HOLE + [:maybe ::HOLE] [:schema ::HOLE] [:schema {:registry {::a ::HOLE}} ::a] [:schema {:registry {::a ::HOLE}} [:ref ::a]] @@ -3600,6 +3601,7 @@ [:tuple ::HOLE :any] [:vector ::HOLE] [:map [:foo ::HOLE]] + [:map [:foo {:optional true} ::HOLE]] [:map [:foo ::HOLE] [:bar :int]] [:and ::HOLE] ;; generator will fail if :any is first [:and ::HOLE :any] @@ -3608,16 +3610,16 @@ [:or ::HOLE :any] [:map-of ::HOLE :any] [:map-of :any ::HOLE] - [:map-of ::HOLE ::HOLE] - ]) + [:map-of ::HOLE ::HOLE]]) -(def simple-parser-schemas [:any [:and :any] :int #'map? :map :tuple [:seqable :any] [:every :catn] - [:fn {:gen/schema :any} #'any?] - [:map-of :any :any]]) -(def transforming-parser-schemas [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]] - #_[:map [:any :any]]]) +(def simple-parser-schemas [:any [:and :any] :int #'map? :tuple [:seqable :any] + [:every [:catn [:any :any]]] ;; bounded => simple + [:fn {:gen/schema :any} #'any?]]) +(def transforming-parser-schemas [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]]]) (deftest parser-info-test + ;; :not has an unreliable generator and is always has a simple parser + (is (every? #(simple-parser? [:not %]) (concat simple-parser-schemas transforming-parser-schemas))) (let [d (m/default-schemas)] (doseq [[hole expected] (concat (map vector simple-parser-schemas (repeat true)) (map vector transforming-parser-schemas (repeat false))) From 459aa941efff3b8e2d61fe850c9b1b89e69f40f8 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 23:00:21 +0000 Subject: [PATCH 16/22] wip --- test/malli/core_test.cljc | 92 ++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 26 deletions(-) diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index d03590f6f..99e27e96f 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3587,8 +3587,8 @@ (defn simple-parser? [s] (boolean (:simple-parser (m/-parser-info (m/schema s))))) -(def simple-parser-templates - "Schemas which have simple parsers iff ::HOLE has a simple parser. +(def inheriting-parser-templates + "Schemas templates which have simple parsers iff ::HOLE has a simple parser. Should also be generatable for any ::HOLE and be capable to (un)parsing to a different value than its input if transforming." [::HOLE @@ -3600,6 +3600,8 @@ [:tuple ::HOLE] [:tuple ::HOLE :any] [:vector ::HOLE] + [:set ::HOLE] + [:seqable ::HOLE] [:map [:foo ::HOLE]] [:map [:foo {:optional true} ::HOLE]] [:map [:foo ::HOLE] [:bar :int]] @@ -3612,39 +3614,77 @@ [:map-of :any ::HOLE] [:map-of ::HOLE ::HOLE]]) -(def simple-parser-schemas [:any [:and :any] :int #'map? :tuple [:seqable :any] - [:every [:catn [:any :any]]] ;; bounded => simple - [:fn {:gen/schema :any} #'any?]]) -(def transforming-parser-schemas [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]]]) +(def simple-parser-templates + "Schema templates which have simple parsers for any value of ::HOLE." + [[:and {:parse 1} ::HOLE :any] + [:and {:parse :none} ::HOLE :any] + [:every ::HOLE] + ]) + +(def transforming-parser-templates + "Schema templates which have transforming parsers for any value of ::HOLE." + [[:multi {:dispatch #'any?} [true ::HOLE]] + [:multi {:dispatch #'boolean} [true :any] [false ::HOLE]] + [:multi {:dispatch #'boolean} [true ::HOLE] [false :any]] + [:andn [0 ::HOLE]] + [:andn [0 ::HOLE] [1 :any]] ;; generator will fail if :any is first + ]) + +(def simple-parser-schemas + "Schemas with simple parsers." + [:any + [:and :any] + :int + #'map? + :tuple + [:fn {:gen/schema :any} #'any?] + [:= 42] [:enum 42] [:not= 42] + [:not [:= (random-uuid)]]]) + +(def transforming-parser-schemas + "Schemas with transforming parsers." + [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]]]) + +(defn ensure-parser-type [expected-simple s] + (let [s (m/schema s) + parse (m/parser s) + unparse (m/parser s)] + (if expected-simple + (doseq [g (is (doall (mg/sample s)))] + (testing (pr-str g) + (let [p (parse g)] + (is (identical? g p)) + (is (identical? g (unparse p)))))) + (is (some (fn [g] + (let [p (parse g)] + (and (not (identical? g p)) + (not (identical? g (unparse p)))))) + (mg/sample s {:seed 0})))))) (deftest parser-info-test - ;; :not has an unreliable generator and is always has a simple parser - (is (every? #(simple-parser? [:not %]) (concat simple-parser-schemas transforming-parser-schemas))) + ;; should really be in simple-parser-templates but :not has an unreliable generator + (testing ":not is simple" + (is (every? #(simple-parser? [:not %]) (concat simple-parser-schemas transforming-parser-schemas))) + (ensure-parser-type true [:not [:= (random-uuid)]]) + (ensure-parser-type true [:not [:andn [:tag [:= (random-uuid)]]]])) + (testing ":multi is transforming" + (is (every? #(simple-parser? [:not %]) (concat simple-parser-schemas transforming-parser-schemas))) + (ensure-parser-type true [:not [:andn [:any [:= (random-uuid)]]]])) (let [d (m/default-schemas)] - (doseq [[hole expected] (concat (map vector simple-parser-schemas (repeat true)) - (map vector transforming-parser-schemas (repeat false))) + (doseq [[hole hold-simple] (concat (map vector simple-parser-schemas (repeat true)) + (map vector transforming-parser-schemas (repeat false))) :let [_ (testing (pr-str hole) - (is (= expected (simple-parser? hole))))] - template simple-parser-templates + (is (= hold-simple (simple-parser? hole))))] + [template expected-simple] (concat (map vector simple-parser-templates (repeat true)) + (map vector transforming-parser-templates (repeat false)) + (map vector inheriting-parser-templates (repeat hold-simple))) :let [s (testing {:template template :hole hole} (is (m/schema template {:registry (assoc d ::HOLE (m/schema hole))})))]] (testing (pr-str (list 'm/schema template {:registry (list 'assoc (list 'm/default-schemas) (symbol "::HOLE") (list 'm/schema hole))})) - (is (= expected (simple-parser? s))) - (let [parse (m/parser s) - unparse (m/parser s)] - (if expected - (doseq [g (is (doall (mg/sample s)))] - (testing (pr-str g) - (let [p (parse g)] - (is (identical? g p)) - (is (identical? g (unparse p)))))) - (is (some (fn [g] - (let [p (parse g)] - (and (not (identical? g p)) - (not (identical? g (unparse p)))))) - (mg/sample s {:seed 0}))))))))) + (is (= expected-simple (simple-parser? s))) + (ensure-parser-type expected-simple s))))) (deftest and-complex-parser-test (is (= {} (m/parse [:and :map [:fn map?]] {}))) From 050c442e1e0ebf59b71ecaa56fdaf12ee40cd3d8 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 23:08:49 +0000 Subject: [PATCH 17/22] move --- test/malli/core_test.cljc | 183 ------------------------------- test/malli/parser_test.cljc | 209 ++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 183 deletions(-) create mode 100644 test/malli/parser_test.cljc diff --git a/test/malli/core_test.cljc b/test/malli/core_test.cljc index 99e27e96f..b2c9d049a 100644 --- a/test/malli/core_test.cljc +++ b/test/malli/core_test.cljc @@ -3585,189 +3585,6 @@ (is (not (m/validate [:seqable {:min 11} :int] (eduction identity (range 10))))) (is (nil? (m/explain [:sequential {:min 9} :int] (eduction identity (range 10)))))) -(defn simple-parser? [s] (boolean (:simple-parser (m/-parser-info (m/schema s))))) - -(def inheriting-parser-templates - "Schemas templates which have simple parsers iff ::HOLE has a simple parser. - Should also be generatable for any ::HOLE and be capable to (un)parsing - to a different value than its input if transforming." - [::HOLE - [:maybe ::HOLE] - [:schema ::HOLE] - [:schema {:registry {::a ::HOLE}} ::a] - [:schema {:registry {::a ::HOLE}} [:ref ::a]] - [:schema {:registry {::a [:ref ::b] ::b ::HOLE}} [:ref ::a]] - [:tuple ::HOLE] - [:tuple ::HOLE :any] - [:vector ::HOLE] - [:set ::HOLE] - [:seqable ::HOLE] - [:map [:foo ::HOLE]] - [:map [:foo {:optional true} ::HOLE]] - [:map [:foo ::HOLE] [:bar :int]] - [:and ::HOLE] ;; generator will fail if :any is first - [:and ::HOLE :any] - [:and ::HOLE :any :any] - [:or ::HOLE] ;; parser will always be identical if :any is first - [:or ::HOLE :any] - [:map-of ::HOLE :any] - [:map-of :any ::HOLE] - [:map-of ::HOLE ::HOLE]]) - -(def simple-parser-templates - "Schema templates which have simple parsers for any value of ::HOLE." - [[:and {:parse 1} ::HOLE :any] - [:and {:parse :none} ::HOLE :any] - [:every ::HOLE] - ]) - -(def transforming-parser-templates - "Schema templates which have transforming parsers for any value of ::HOLE." - [[:multi {:dispatch #'any?} [true ::HOLE]] - [:multi {:dispatch #'boolean} [true :any] [false ::HOLE]] - [:multi {:dispatch #'boolean} [true ::HOLE] [false :any]] - [:andn [0 ::HOLE]] - [:andn [0 ::HOLE] [1 :any]] ;; generator will fail if :any is first - ]) - -(def simple-parser-schemas - "Schemas with simple parsers." - [:any - [:and :any] - :int - #'map? - :tuple - [:fn {:gen/schema :any} #'any?] - [:= 42] [:enum 42] [:not= 42] - [:not [:= (random-uuid)]]]) - -(def transforming-parser-schemas - "Schemas with transforming parsers." - [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]]]) - -(defn ensure-parser-type [expected-simple s] - (let [s (m/schema s) - parse (m/parser s) - unparse (m/parser s)] - (if expected-simple - (doseq [g (is (doall (mg/sample s)))] - (testing (pr-str g) - (let [p (parse g)] - (is (identical? g p)) - (is (identical? g (unparse p)))))) - (is (some (fn [g] - (let [p (parse g)] - (and (not (identical? g p)) - (not (identical? g (unparse p)))))) - (mg/sample s {:seed 0})))))) - -(deftest parser-info-test - ;; should really be in simple-parser-templates but :not has an unreliable generator - (testing ":not is simple" - (is (every? #(simple-parser? [:not %]) (concat simple-parser-schemas transforming-parser-schemas))) - (ensure-parser-type true [:not [:= (random-uuid)]]) - (ensure-parser-type true [:not [:andn [:tag [:= (random-uuid)]]]])) - (testing ":multi is transforming" - (is (every? #(simple-parser? [:not %]) (concat simple-parser-schemas transforming-parser-schemas))) - (ensure-parser-type true [:not [:andn [:any [:= (random-uuid)]]]])) - (let [d (m/default-schemas)] - (doseq [[hole hold-simple] (concat (map vector simple-parser-schemas (repeat true)) - (map vector transforming-parser-schemas (repeat false))) - :let [_ (testing (pr-str hole) - (is (= hold-simple (simple-parser? hole))))] - [template expected-simple] (concat (map vector simple-parser-templates (repeat true)) - (map vector transforming-parser-templates (repeat false)) - (map vector inheriting-parser-templates (repeat hold-simple))) - :let [s (testing {:template template :hole hole} - (is (m/schema template {:registry (assoc d ::HOLE (m/schema hole))})))]] - (testing (pr-str (list 'm/schema template - {:registry (list 'assoc (list 'm/default-schemas) - (symbol "::HOLE") (list 'm/schema hole))})) - (is (= expected-simple (simple-parser? s))) - (ensure-parser-type expected-simple s))))) - -(deftest and-complex-parser-test - (is (= {} (m/parse [:and :map [:fn map?]] {}))) - (is (= {} (m/parse [:and [:fn map?] :map] {}))) - (is (= #malli.core.Tag{:key :left, :value 1} (m/parse [:and [:orn [:left :int] [:right :int]] [:fn number?]] 1))) - (is (= #malli.core.Tag{:key :left, :value 1} (m/parse [:and [:fn number?] [:orn [:left :int] [:right :int]]] 1))) - (is (= 1 (m/parse [:and {:parse :none} [:fn number?] [:orn [:left :int] [:right :int]]] 1))) - (is (= 1 (m/parse [:and :int [:or :int :boolean]] 1))) - (is (= 1 (m/parse [:and [:or :int :boolean] :int] 1))) - (is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and :int [:orn [:int :int] [:boolean :boolean]]] 1))) - (is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and [:orn [:int :int] [:boolean :boolean]] :int] 1))) - (is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and [:and [:orn [:int :int] [:boolean :boolean]] :int] :int] 1))) - (is (= #malli.core.Tag{:key :l, :value #malli.core.Tag{:key :int, :value 1}} - (m/parse [:and - [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] - :int] 1))) - (is (= 1 - (m/parse [:and - {:parse :none} - [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] - [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]] - 1))) - (is (= #malli.core.Tag{:key :l, :value #malli.core.Tag{:key :int, :value 1}} - (m/parse [:and - {:parse 0} - [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] - [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]] - 1))) - (is (= #malli.core.Tag{:key :r, :value #malli.core.Tag{:key :int, :value 1}} - (m/parse [:and - {:parse 1} - [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] - [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]] - 1))) - (let [s [:and [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] :int]] - (is (= 1 (->> 1 (m/parse s) (m/unparse s))))) - (let [s [:and - {:parse 1} - [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] - [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]]] - (is (= 1 (->> 1 (m/parse s) (m/unparse s))))) - (is (m/parser [:and [:map] [:map]])) - (is (m/parser [:and [:map [:left [:orn [:one :int]]]] [:map]])) - (is (m/parser [:and [:map] [:map [:left [:orn [:one :int]]]]])) - (is (thrown-with-msg? - #?(:clj Exception, :cljs js/Error) - #":malli\.core/and-schema-multiple-transforming-parsers" - (m/parser [:and [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]))) - (is (-> (m/schema [:vector :int]) m/-parser-info :simple-parser)) - (is (-> (m/schema [:vector [:orn [:one :int]]]) m/-parser-info :simple-parser not)) - (is (= #malli.core.Tags{:values {"a" 3, "b" :x}} - (m/parse [:and [:catn ["a" :int] ["b" :keyword]] - [:fn vector?]] - [3 :x]))) - (let [s [:and [:catn ["a" :int] ["b" :keyword]] - [:fn vector?]] - res (->> [3 :x] - (m/parse s) - (m/unparse s))] - (is (= [3 :x] res)) - (is (m/validate s res))) - (let [s [:and [:catn ["a" :int] ["b" :keyword]] - [:vector :any]] - res (->> [3 :x] - (m/parse s) - (m/unparse s))] - (is (= [3 :x] res)) - (is (m/validate s res))) - (let [s [:and [:catn ["a" :int] ["b" :keyword]] - [:sequential :any]] - res (->> [3 :x] - (m/parse s) - (m/unparse s))] - (is (= [3 :x] res)) - (is (m/validate s res))) - (let [s [:and [:catn ["a" :int] ["b" :keyword]] - [:tuple :any :any]] - res (->> [3 :x] - (m/parse s) - (m/unparse s))] - (is (= [3 :x] res)) - (is (m/validate s res)))) - (deftest andn-test (is (= {:schema [:andn [:m :map] [:v [:vector :any]]], :value {}, diff --git a/test/malli/parser_test.cljc b/test/malli/parser_test.cljc new file mode 100644 index 000000000..10243ff9c --- /dev/null +++ b/test/malli/parser_test.cljc @@ -0,0 +1,209 @@ +(ns malli.parser-test + (:require [clojure.string :as str] + [clojure.test :refer [are deftest is testing]] + [clojure.test.check.generators :as gen] + [clojure.walk :as walk] + [malli.core :as m] + [malli.edn :as edn] + [malli.generator :as mg] + [malli.error :as me] + [malli.impl.util :as miu] + [malli.registry :as mr] + [malli.transform :as mt] + [malli.util :as mu] + #?(:clj [malli.test-macros :refer [when-env]])) + #?(:clj (:import (clojure.lang IFn PersistentArrayMap PersistentHashMap)) + :cljs (:require-macros [malli.test-macros :refer [when-env]]))) + +(defn simple-parser? [s] (boolean (:simple-parser (m/-parser-info (m/schema s))))) + +(def inheriting-parser-templates + "Schemas templates which have simple parsers iff ::HOLE has a simple parser. + Should also be generatable for any ::HOLE and be capable to (un)parsing + to a different value than its input if transforming." + [::HOLE + [:maybe ::HOLE] + [:schema ::HOLE] + [:schema {:registry {::a ::HOLE}} ::a] + [:schema {:registry {::a ::HOLE}} [:ref ::a]] + [:schema {:registry {::a [:ref ::b] ::b ::HOLE}} [:ref ::a]] + [:tuple ::HOLE] + [:tuple ::HOLE :any] + [:vector ::HOLE] + [:set ::HOLE] + [:seqable ::HOLE] + [:map [:foo ::HOLE]] + [:map [:foo {:optional true} ::HOLE]] + [:map [:foo ::HOLE] [:bar :int]] + [:and ::HOLE] ;; generator will fail if :any is first + [:and ::HOLE :any] + [:and ::HOLE :any :any] + [:or ::HOLE] ;; parser will always be identical if :any is first + [:or ::HOLE :any] + [:map-of ::HOLE :any] + [:map-of :any ::HOLE] + [:map-of ::HOLE ::HOLE]]) + +(def simple-parser-templates + "Schema templates which have simple parsers for any value of ::HOLE." + [[:and {:parse 1} ::HOLE :any] + [:and {:parse :none} ::HOLE :any] + [:every ::HOLE] + [:-> ::HOLE] + [:function [:-> ::HOLE]] + ]) + +(def transforming-parser-templates + "Schema templates which have transforming parsers for any value of ::HOLE." + [[:multi {:dispatch #'any?} [true ::HOLE]] + [:multi {:dispatch #'boolean} [true :any] [false ::HOLE]] + [:multi {:dispatch #'boolean} [true ::HOLE] [false :any]] + [:andn [0 ::HOLE]] + [:andn [0 ::HOLE] [1 :any]] ;; generator will fail if :any is first + [:orn [0 ::HOLE]] + [:orn [0 ::HOLE] [1 :any]] + [:orn [0 :any] [1 ::HOLE]] + [:orn [0 ::HOLE] [1 ::HOLE]]]) + +(def simple-parser-schemas + "Schemas with simple parsers." + [:any + [:and :any] + :int + #'map? + :tuple + [:fn {:gen/schema :any} #'any?] + [:= 42] [:enum 42] [:not= 42] [:< 5] [:> 5] [:<= 5] [:>= 5] + [:re #""] + :nil + :qualified-symbol + :uuid + [:not [:= (random-uuid)]] ;; generator is too unreliable to nest + :some]) + +(def transforming-parser-schemas + "Schemas with transforming parsers." + [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]]]) + +(defn ensure-parser-type [expected-simple s] + (let [s (m/schema s) + parse (m/parser s) + unparse (m/parser s)] + (if expected-simple + (doseq [g (is (doall (mg/sample s)))] + (testing (pr-str g) + (let [p (parse g)] + (is (identical? g p)) + (is (identical? g (unparse p)))))) + (is (some (fn [g] + (let [p (parse g)] + (and (not (identical? g p)) + (not (identical? g (unparse p)))))) + (mg/sample s {:seed 0})))))) + +(deftest parser-info-test + ;; should really be in simple-parser-templates but :not has an unreliable generator + (testing ":not is simple" + (is (every? #(simple-parser? [:not %]) (concat simple-parser-schemas transforming-parser-schemas))) + (ensure-parser-type true [:not [:= (random-uuid)]]) + (ensure-parser-type true [:not [:andn [:tag [:= (random-uuid)]]]])) + (testing ":multi is transforming" + (is (every? #(simple-parser? [:not %]) (concat simple-parser-schemas transforming-parser-schemas))) + (ensure-parser-type true [:not [:andn [:any [:= (random-uuid)]]]])) + (let [d (m/default-schemas)] + (doseq [[hole hold-simple] (concat (map vector simple-parser-schemas (repeat true)) + (map vector transforming-parser-schemas (repeat false))) + :let [_ (testing (pr-str hole) + (is (= hold-simple (simple-parser? hole))))] + [template expected-simple] (concat (map vector simple-parser-templates (repeat true)) + (map vector transforming-parser-templates (repeat false)) + (map vector inheriting-parser-templates (repeat hold-simple))) + :let [s (testing {:template template :hole hole} + (is (m/schema template {:registry (assoc d ::HOLE (m/schema hole))})))]] + (testing (pr-str (list 'm/schema template + {:registry (list 'assoc (list 'm/default-schemas) + (symbol "::HOLE") (list 'm/schema hole))})) + (is (= expected-simple (simple-parser? s))) + (ensure-parser-type expected-simple s))))) + +(deftest and-complex-parser-test + (is (= {} (m/parse [:and :map [:fn map?]] {}))) + (is (= {} (m/parse [:and [:fn map?] :map] {}))) + (is (= #malli.core.Tag{:key :left, :value 1} (m/parse [:and [:orn [:left :int] [:right :int]] [:fn number?]] 1))) + (is (= #malli.core.Tag{:key :left, :value 1} (m/parse [:and [:fn number?] [:orn [:left :int] [:right :int]]] 1))) + (is (= 1 (m/parse [:and {:parse :none} [:fn number?] [:orn [:left :int] [:right :int]]] 1))) + (is (= 1 (m/parse [:and :int [:or :int :boolean]] 1))) + (is (= 1 (m/parse [:and [:or :int :boolean] :int] 1))) + (is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and :int [:orn [:int :int] [:boolean :boolean]]] 1))) + (is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and [:orn [:int :int] [:boolean :boolean]] :int] 1))) + (is (= #malli.core.Tag{:key :int, :value 1} (m/parse [:and [:and [:orn [:int :int] [:boolean :boolean]] :int] :int] 1))) + (is (= #malli.core.Tag{:key :l, :value #malli.core.Tag{:key :int, :value 1}} + (m/parse [:and + [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] + :int] 1))) + (is (= 1 + (m/parse [:and + {:parse :none} + [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] + [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]] + 1))) + (is (= #malli.core.Tag{:key :l, :value #malli.core.Tag{:key :int, :value 1}} + (m/parse [:and + {:parse 0} + [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] + [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]] + 1))) + (is (= #malli.core.Tag{:key :r, :value #malli.core.Tag{:key :int, :value 1}} + (m/parse [:and + {:parse 1} + [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] + [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]] + 1))) + (let [s [:and [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] :int]] + (is (= 1 (->> 1 (m/parse s) (m/unparse s))))) + (let [s [:and + {:parse 1} + [:orn [:l [:and [:orn [:int :int] [:boolean :boolean]] :int]]] + [:orn [:r [:and [:orn [:int :int] [:boolean :boolean]] :int]]]]] + (is (= 1 (->> 1 (m/parse s) (m/unparse s))))) + (is (m/parser [:and [:map] [:map]])) + (is (m/parser [:and [:map [:left [:orn [:one :int]]]] [:map]])) + (is (m/parser [:and [:map] [:map [:left [:orn [:one :int]]]]])) + (is (thrown-with-msg? + #?(:clj Exception, :cljs js/Error) + #":malli\.core/and-schema-multiple-transforming-parsers" + (m/parser [:and [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]))) + (is (-> (m/schema [:vector :int]) m/-parser-info :simple-parser)) + (is (-> (m/schema [:vector [:orn [:one :int]]]) m/-parser-info :simple-parser not)) + (is (= #malli.core.Tags{:values {"a" 3, "b" :x}} + (m/parse [:and [:catn ["a" :int] ["b" :keyword]] + [:fn vector?]] + [3 :x]))) + (let [s [:and [:catn ["a" :int] ["b" :keyword]] + [:fn vector?]] + res (->> [3 :x] + (m/parse s) + (m/unparse s))] + (is (= [3 :x] res)) + (is (m/validate s res))) + (let [s [:and [:catn ["a" :int] ["b" :keyword]] + [:vector :any]] + res (->> [3 :x] + (m/parse s) + (m/unparse s))] + (is (= [3 :x] res)) + (is (m/validate s res))) + (let [s [:and [:catn ["a" :int] ["b" :keyword]] + [:sequential :any]] + res (->> [3 :x] + (m/parse s) + (m/unparse s))] + (is (= [3 :x] res)) + (is (m/validate s res))) + (let [s [:and [:catn ["a" :int] ["b" :keyword]] + [:tuple :any :any]] + res (->> [3 :x] + (m/parse s) + (m/unparse s))] + (is (= [3 :x] res)) + (is (m/validate s res)))) From 361eb5e0d22e535a8a971c680cff9c0899cdfc7e Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 23:11:26 +0000 Subject: [PATCH 18/22] wip --- test/bb_test_runner.clj | 2 ++ test/malli/parser_test.cljc | 32 ++++++++++++++++---------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/test/bb_test_runner.clj b/test/bb_test_runner.clj index f273299a0..7cdefd31f 100644 --- a/test/bb_test_runner.clj +++ b/test/bb_test_runner.clj @@ -10,6 +10,7 @@ [malli.generator-test] [malli.instrument-test] [malli.json-schema-test] + [malli.parser-test] [malli.plantuml-test] [malli.provider-test] [malli.registry-test] @@ -29,6 +30,7 @@ 'malli.instrument-test 'malli.json-schema-test ;; 'malli.generator-test ;; skipped for now due to test.chuck incompatibility + 'malli.parser-test 'malli.plantuml-test 'malli.provider-test 'malli.registry-test diff --git a/test/malli/parser_test.cljc b/test/malli/parser_test.cljc index 10243ff9c..df06e8f73 100644 --- a/test/malli/parser_test.cljc +++ b/test/malli/parser_test.cljc @@ -50,8 +50,7 @@ [:and {:parse :none} ::HOLE :any] [:every ::HOLE] [:-> ::HOLE] - [:function [:-> ::HOLE]] - ]) + [:function [:-> ::HOLE]]]) (def transforming-parser-templates "Schema templates which have transforming parsers for any value of ::HOLE." @@ -86,20 +85,21 @@ [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]]]) (defn ensure-parser-type [expected-simple s] - (let [s (m/schema s) - parse (m/parser s) - unparse (m/parser s)] - (if expected-simple - (doseq [g (is (doall (mg/sample s)))] - (testing (pr-str g) - (let [p (parse g)] - (is (identical? g p)) - (is (identical? g (unparse p)))))) - (is (some (fn [g] - (let [p (parse g)] - (and (not (identical? g p)) - (not (identical? g (unparse p)))))) - (mg/sample s {:seed 0})))))) + #?(:bb nil ;;FIXME test.chuck incompatibility + :default (let [s (m/schema s) + parse (m/parser s) + unparse (m/parser s)] + (if expected-simple + (doseq [g (is (doall (mg/sample s)))] + (testing (pr-str g) + (let [p (parse g)] + (is (identical? g p)) + (is (identical? g (unparse p)))))) + (is (some (fn [g] + (let [p (parse g)] + (and (not (identical? g p)) + (not (identical? g (unparse p)))))) + (mg/sample s {:seed 0}))))))) (deftest parser-info-test ;; should really be in simple-parser-templates but :not has an unreliable generator From d0d45ccee0c99596433a8c9f60ebad9fe1360efb Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 23:13:53 +0000 Subject: [PATCH 19/22] fix https://github.com/metosin/malli/issues/1173 --- src/malli/generator.cljc | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/malli/generator.cljc b/src/malli/generator.cljc index cf6028dfa..79fa9ef6d 100644 --- a/src/malli/generator.cljc +++ b/src/malli/generator.cljc @@ -106,10 +106,11 @@ (defn- gen-vector-distinct-by [schema {:keys [min] :as m} f g] (if (-unreachable-gen? g) (if (= 0 (or min 0)) (gen/return []) g) - (gen/vector-distinct-by f g (-> (assoc (if (and min (= min max)) - {:num-elements min} - (set/rename-keys m {:min :min-elements :max :max-elements})) - :ex-fn #(m/-exception ::distinct-generator-failure (assoc % :schema schema))))))) + (gen/vector-distinct-by f g (into (if (and min (= min max)) + {:num-elements min} + (set/rename-keys m {:min :min-elements :max :max-elements})) + {:max-tries 100 + :ex-fn #(m/-exception ::distinct-generator-failure (assoc % :schema schema))})))) (defn- -string-gen [schema options] (gen-fmap str/join (gen-vector (-min-max schema options) gen/char-alphanumeric))) From ff56e32c992ebf8e898f66428f8582018e419950 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Fri, 28 Mar 2025 23:17:06 +0000 Subject: [PATCH 20/22] wip --- test/malli/parser_test.cljc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/malli/parser_test.cljc b/test/malli/parser_test.cljc index df06e8f73..f25c77efe 100644 --- a/test/malli/parser_test.cljc +++ b/test/malli/parser_test.cljc @@ -73,7 +73,7 @@ :tuple [:fn {:gen/schema :any} #'any?] [:= 42] [:enum 42] [:not= 42] [:< 5] [:> 5] [:<= 5] [:>= 5] - [:re #""] + #?@(:cljs [] :default [[:re #""]]) ;; no generator in cljs :nil :qualified-symbol :uuid From 4a9f5abbece6b01e7ebad04cc71a2d076de82ba8 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 1 Apr 2025 05:32:45 +0000 Subject: [PATCH 21/22] fix m/parse examples, better bb explanation --- README.md | 15 ++++++++++++--- test/malli/parser_test.cljc | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4e9da83f0..ef29a9e2b 100644 --- a/README.md +++ b/README.md @@ -2538,13 +2538,22 @@ to identify the conjunct by index. To opt-out of parsing any further levels of this schema, use the `:parse :none` property. ```clojure -(m/parse [:and {:parse 0} [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]) +(m/parse [:and {:parse 0} + [:map [:left [:orn [:one :int]]]] + [:map [:right [:orn [:one :int]]]]] + {:left 1 :right 1}) ; => {:left #malli.core.Tag{:key :one, :value 1}, :right 1} -(m/parse [:and {:parse 1} [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]) +(m/parse [:and {:parse 1} + [:map [:left [:orn [:one :int]]]] + [:map [:right [:orn [:one :int]]]]] + {:left 1 :right 1}) ; => {:left 1, :right #malli.core.Tag{:key :one, :value 1}} -(m/parse [:and {:parse :none} [:map [:left [:orn [:one :int]]]] [:map [:right [:orn [:one :int]]]]]) +(m/parse [:and {:parse :none} + [:map [:left [:orn [:one :int]]]] + [:map [:right [:orn [:one :int]]]]] + {:left 1 :right 1}) ; => {:left 1, :right 1} ``` diff --git a/test/malli/parser_test.cljc b/test/malli/parser_test.cljc index f25c77efe..93f1fb9c7 100644 --- a/test/malli/parser_test.cljc +++ b/test/malli/parser_test.cljc @@ -85,7 +85,7 @@ [[:andn [:any :any]] [:catn [:any :any]] [:seqable [:catn [:any :any]]] [:multi {:dispatch #'any?} [true :any]]]) (defn ensure-parser-type [expected-simple s] - #?(:bb nil ;;FIXME test.chuck incompatibility + #?(:bb nil ;; test.chuck doesn't work in bb :default (let [s (m/schema s) parse (m/parser s) unparse (m/parser s)] From 594313639e2ad4ba3e96f9ac12d4c4557160ee33 Mon Sep 17 00:00:00 2001 From: Ambrose Bonnaire-Sergeant Date: Tue, 1 Apr 2025 05:45:16 +0000 Subject: [PATCH 22/22] fix comment --- test/malli/parser_test.cljc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/malli/parser_test.cljc b/test/malli/parser_test.cljc index 93f1fb9c7..3f1d5c819 100644 --- a/test/malli/parser_test.cljc +++ b/test/malli/parser_test.cljc @@ -18,8 +18,8 @@ (defn simple-parser? [s] (boolean (:simple-parser (m/-parser-info (m/schema s))))) (def inheriting-parser-templates - "Schemas templates which have simple parsers iff ::HOLE has a simple parser. - Should also be generatable for any ::HOLE and be capable to (un)parsing + "Schema templates which have simple parsers iff ::HOLE has a simple parser. + Should also be generatable for any ::HOLE and have high likelihood of (un)parsing to a different value than its input if transforming." [::HOLE [:maybe ::HOLE]