Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use custom Tag / Tags records (instead of MapEntry / Map) in parse output #1150

Merged
merged 6 commits into from
Jan 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino

Malli is in well matured [alpha](README.md#alpha).

##
## UNRELEASED

* Docs: elaborate optional-keys and required-keys [#1117](https://github.com/metosin/malli/pull/1117)
* **BREAKING** Output of `parse` now uses new `malli.core.Tag` and `malli.core.Tags` records for `:orn`, `:multi`, `:altn`, `:catn` etc. [#1123](https://github.com/metosin/malli/issues/1123) [#1153](https://github.com/metosin/malli/issues/1153)
* See [Parsing](#parsing-values) and [Unparsing](#unparsing-values) for docs.

## 0.17.0 (2024-12-08)

Expand Down
48 changes: 36 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2445,9 +2445,10 @@ Schemas can be used to parse values using `m/parse` and `m/parser`:
[:s string?]
[:b boolean?]]]]]
["-server" "foo" "-verbose" true "-user" "joe"])
;[{:prop "-server", :val [:s "foo"]}
; {:prop "-verbose", :val [:b true]}
; {:prop "-user", :val [:s "joe"]}]
;[#malli.core.Tags{:values {:prop "-server", :val #malli.core.Tag{:key :s, :value "foo"}}}
; #malli.core.Tags{:values {:prop "-verbose", :val #malli.core.Tag{:key :b, :value true}}}
; #malli.core.Tags{:values {:prop "-user", :val #malli.core.Tag{:key :s, :value "joe"}}}]

```

`m/parser` to create an optimized parser:
Expand All @@ -2471,13 +2472,25 @@ Schemas can be used to parse values using `m/parse` and `m/parser`:
(parse-hiccup
[:div {:class [:foo :bar]}
[:p "Hello, world of data"]])
;[:node
; {:name :div
; :props {:class [:foo :bar]}
; :children [[:node
; {:name :p
; :props nil
; :children [[:primitive [:text "Hello, world of data"]]]}]]}]

;#malli.core.Tag
;{:key :node,
; :value
; #malli.core.Tags
; {:values {:name :div,
; :props {:class [:foo :bar]},
; :children [#malli.core.Tag
; {:key :node,
; :value
; #malli.core.Tags
; {:values {:name :p,
; :props nil,
; :children [#malli.core.Tag
; {:key :primitive,
; :value
; #malli.core.Tag
; {:key :text,
; :value "Hello, world of data"}}]}}}]}}}
```

Parsing returns tagged values for `:orn`, `:catn`, `:altn` and `:multi`.
Expand All @@ -2489,10 +2502,10 @@ Parsing returns tagged values for `:orn`, `:catn`, `:altn` and `:multi`.
[::m/default :any]])

(m/parse Multi {:type :user, :size 1})
; => [:user {:type :user, :size 1}]
; => #malli.core.Tag{:key :user, :value {:type :user, :size 1}}

(m/parse Multi {:type "sized", :size 1})
; => [:malli.core/default {:type "sized", :size 1}]
; => #malli.core.Tag{:key :malli.core/default, :value {:type "sized", :size 1}}
```

## Unparsing values
Expand All @@ -2508,6 +2521,17 @@ The inverse of parsing, using `m/unparse` and `m/unparser`:
; [:p "Hello, world of data"]]
```

```clojure
(m/unparse [:orn [:name :string] [:id :int]]
(m/tagged :name "x"))
; => "x"

(m/unparse [:* [:catn [:name :string] [:id :int]]]
[(m/tags {:name "x" :id 1})
(m/tags {:name "y" :id 2})])
; => ["x" 1 "y" 2]
```

## Serializable functions

Enabling serializable function schemas requires [SCI](https://github.com/borkdude/sci) or [cherry](https://github.com/squint-cljs/cherry) (for client side) as external dependency. If
Expand Down
49 changes: 36 additions & 13 deletions src/malli/core.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,27 @@
#?(:clj (defmethod print-method ::into-schema [v ^java.io.Writer w] (.write w (str "#IntoSchema{:type " (pr-str (-type ^IntoSchema v)) "}"))))
#?(:clj (defmethod print-method ::schema [v ^java.io.Writer w] (.write w (pr-str (-form ^Schema v)))))

(defrecord Tag [key value])

(defn tag
"A tagged value, used eg. for results of `parse` for `:orn` schemas."
[key value] (->Tag key value))

(defn tag?
"Is this a value constructed with `tag`?"
[x] (instance? Tag x))

(defrecord Tags [values])

(defn tags
"A collection of tagged values. `values` should be a map from tag to value.
Used eg. for results of `parse` for `:catn` schemas."
[values] (->Tags values))

(defn tags?
"Is this a value constructed with `tags`?"
[x] (instance? Tags x))

;;
;; impl
;;
Expand Down Expand Up @@ -385,7 +406,7 @@
;;

(defn -simple-entry-parser [keyset children forms]
(let [entries (map (fn [[k p s]] (miu/-tagged k (-val-schema s p))) children)]
(let [entries (map (fn [[k p s]] (miu/-entry k (-val-schema s p))) children)]
(reify EntryParser
(-entry-keyset [_] keyset)
(-entry-children [_] children)
Expand Down Expand Up @@ -606,7 +627,7 @@
(reify EntryParser
(-entry-keyset [_] keyset)
(-entry-children [_] @children)
(-entry-entries [_] (-vmap (fn [[k p s]] (miu/-tagged k (-val-schema s p))) @children))
(-entry-entries [_] (-vmap (fn [[k p s]] (miu/-entry k (-val-schema s p))) @children))
(-entry-forms [_] (->> @children (-vmap (fn [[k p v]] (if p [k p (-form v)] [k (-form v)]))))))))

(defn -from-entry-ast [parent ast options]
Expand Down Expand Up @@ -865,15 +886,15 @@
(-parser [this]
(let [parsers (-vmap (fn [[k _ c]]
(let [c (-parser c)]
(fn [x] (miu/-map-valid #(reduced (miu/-tagged k %)) (c x)))))
(fn [x] (miu/-map-valid #(reduced (tag k %)) (c x)))))
(-children this))]
(fn [x] (reduce (fn [_ parser] (parser x)) x parsers))))
(-unparser [this]
(let [unparsers (into {} (map (fn [[k _ c]] [k (-unparser c)])) (-children this))]
(fn [x]
(if (miu/-tagged? x)
(if-some [unparse (get unparsers (key x))]
(unparse (val x))
(if (tag? x)
(if-some [unparse (get unparsers (:key x))]
(unparse (:value x))
::invalid)
::invalid))))
(-transformer [this transformer method options]
Expand Down Expand Up @@ -1011,6 +1032,8 @@
->parser (fn [this f]
(let [keyset (-entry-keyset (-entry-parser this))
default-parser (some-> @default-schema (f))
;; prevent unparsing :catn/:orn/etc parse results as maps
ok? #(and (pred? %) (not (tag? %)) (not (tags? %)))
parsers (cond->> (-vmap
(fn [[key {:keys [optional]} schema]]
(let [parser (f schema)]
Expand All @@ -1035,7 +1058,7 @@
(reduce
(fn [m k] (if (contains? keyset k) m (reduced (reduced ::invalid))))
m (keys m)))))]
(fn [x] (if (pred? x) (reduce (fn [m parser] (parser m)) x parsers) ::invalid))))]
(fn [x] (if (ok? x) (reduce (fn [m parser] (parser m)) x parsers) ::invalid))))]
^{:type ::schema}
(reify
AST
Expand Down Expand Up @@ -1645,12 +1668,12 @@
(let [->path (if (and (map? x) (keyword? dispatch)) #(conj % dispatch) identity)]
(conj acc (miu/-error (->path path) (->path in) this x ::invalid-dispatch-value)))))))
(-parser [_]
(let [parse (fn [k s] (let [p (-parser s)] (fn [x] (miu/-map-valid #(miu/-tagged k %) (p x)))))
(let [parse (fn [k s] (let [p (-parser s)] (fn [x] (miu/-map-valid #(tag k %) (p x)))))
find (finder (reduce-kv (fn [acc k s] (assoc acc k (parse k s))) {} @dispatch-map))]
(fn [x] (if-some [parser (find (dispatch x))] (parser x) ::invalid))))
(-unparser [_]
(let [unparsers (reduce-kv (fn [acc k s] (assoc acc k (-unparser s))) {} @dispatch-map)]
(fn [x] (if (miu/-tagged? x) (if-some [f (unparsers (key x))] (f (val x)) ::invalid) ::invalid))))
(fn [x] (if (tag? x) (if-some [f (unparsers (:key x))] (f (:value x)) ::invalid) ::invalid))))
(-transformer [this transformer method options]
;; FIXME: Probably should not use `dispatch`
;; Can't use `dispatch` as `x` might not be valid before it has been unparsed:
Expand Down Expand Up @@ -2683,15 +2706,15 @@
:catn (-sequence-entry-schema {:type :catn, :child-bounds {}, :keep false
:re-validator (fn [_ children] (apply re/cat-validator children))
:re-explainer (fn [_ children] (apply re/cat-explainer children))
:re-parser (fn [_ children] (apply re/catn-parser children))
:re-unparser (fn [_ children] (apply re/catn-unparser children))
:re-parser (fn [_ children] (apply re/catn-parser tags children))
:re-unparser (fn [_ children] (apply re/catn-unparser tags? children))
:re-transformer (fn [_ children] (apply re/cat-transformer children))
:re-min-max (fn [_ children] (reduce (partial -re-min-max +) {:min 0, :max 0} (-vmap last children)))})
:altn (-sequence-entry-schema {:type :altn, :child-bounds {:min 1}, :keep false
:re-validator (fn [_ children] (apply re/alt-validator children))
:re-explainer (fn [_ children] (apply re/alt-explainer children))
:re-parser (fn [_ children] (apply re/altn-parser children))
:re-unparser (fn [_ children] (apply re/altn-unparser children))
:re-parser (fn [_ children] (apply re/altn-parser tag children))
:re-unparser (fn [_ children] (apply re/altn-unparser tag? children))
:re-transformer (fn [_ children] (apply re/alt-transformer children))
:re-min-max (fn [_ children] (reduce -re-alt-min-max {:max 0} (-vmap last children)))})})

Expand Down
17 changes: 8 additions & 9 deletions src/malli/destructure.cljc
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
(ns malli.destructure
(:require [clojure.walk :as walk]
[malli.core :as m]
[malli.impl.util :as miu]))
[malli.core :as m]))

(defn -map-like? [x] (or (map? x) (and (seqable? x) (every? (fn [e] (and (vector? e) (= 2 (count e)))) x))))
(defn -qualified-key? [k] (and (qualified-keyword? k) (-> k name #{"keys" "syms"})))
Expand Down Expand Up @@ -62,10 +61,10 @@
(defn -any? [x] (= :any x))
(defn -maybe? [x] (and (vector? x) (= :maybe (first x))))

(defn -vector [{:keys [as elems rest]} options]
(or (some->> as :schema :schema (conj [:schema]))
(defn -vector [{{:keys [as elems rest]} :values} options]
(or (some->> as :values :schema :values :schema (conj [:schema]))
(let [ess (map #(let [s (-transform % options false)] (cond->> s (not (-maybe? s)) (conj [:?]))) elems)
rs (if rest (-transform (:arg rest) options true) [:* :any])]
rs (if rest (-transform (:arg (:values rest)) options true) [:* :any])]
[:maybe (if (seq ess) (-> [:cat] (into ess) (conj rs)) [:cat rs])])))

(defn -qualified-keys [m]
Expand All @@ -78,7 +77,7 @@
(let [any (fn [f ks] (map (fn [k] [(f k) :any]) ks))]
(->> (concat (any keyword keys) (any str strs) (any identity syms)
(map (fn [k] [k (if (and references (qualified-keyword? k)) k :any)]) (-qualified-keys arg))
(map (fn [[k v]] [v (-transform {:arg k} options false)]) (filter #(miu/-tagged? (key %)) arg)))
(map (fn [[k v]] [v (-transform (m/tags {:arg k}) options false)]) (filter #(m/tag? (key %)) arg)))
(distinct))))

(defn -map [arg {:keys [::references ::required-keys ::closed-maps ::sequential-maps]
Expand All @@ -96,19 +95,19 @@
(cond->> :always (conj [:*]) (not rest) (conj [:schema])))]]
schema)))

(defn -transform [{[k v] :arg schema :schema :as all} options rest]
(defn -transform [{{{k :key v :value} :arg schema :schema :as all} :values} options rest]
(cond (and schema rest) (let [s (-transform all options false)] (if (-any? s) schema s))
schema schema
(= :vec k) (-vector v options)
(= :map k) (-map v options rest)
rest [:* :any]
:else :any))

(defn -schema [{:keys [elems rest]} options]
(defn -schema [{{:keys [elems rest]} :values} options]
(cond-> :cat
(or (seq elems) rest) (vector)
(seq elems) (into (map #(-transform % options false) elems))
rest (conj (-transform (:arg rest) options true))))
rest (conj (-transform (:arg (:values rest)) options true))))

(defn -unschematize [x]
(walk/prewalk #(cond-> % (and (map? %) (:- %)) (dissoc :- :schema)) x))
Expand Down
11 changes: 6 additions & 5 deletions src/malli/experimental.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@
(def Params (-schema false))

(c/defn -defn [schema args]
(let [{:keys [name return doc arities] body-meta :meta :as parsed} (m/parse schema args)
(let [{:keys [name return doc arities] body-meta :meta :as parsed} (:values (m/parse schema args))
return (:values return)
var-meta (meta name)
_ (when (= ::m/invalid parsed) (m/-fail! ::parse-error {:schema schema, :args args}))
parse (fn [{:keys [args] :as parsed}] (merge (md/parse args) parsed))
parse (fn [parsed] (merge (md/parse (-> parsed :values :args)) (:values parsed)))
->schema (fn [{:keys [schema]}] [:=> schema (:schema return :any)])
single (= :single (key arities))
parglists (if single (->> arities val parse vector) (->> arities val :arities (map parse)))
single (= :single (:key arities))
parglists (if single (->> arities :value parse vector) (->> arities :value :values :arities (map parse)))
raw-arglists (map :raw-arglist parglists)
schema (as-> (map ->schema parglists) $ (if single (first $) (into [:function] $)))
bodies (map (fn [{:keys [arglist prepost body]}] `(~arglist ~prepost ~@body)) parglists)
Expand All @@ -60,7 +61,7 @@
~@(some-> doc vector)
~enriched-meta
~@bodies
~@(when-not single (some->> arities val :meta vector))))]
~@(when-not single (some->> arities :value :meta vector))))]
(m/=> ~name ~schema)
defn#)))

Expand Down
34 changes: 19 additions & 15 deletions src/malli/impl/regex.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -172,14 +172,15 @@
(reverse (cons r rs)))]
(fn [driver regs pos coll k] (sp driver regs [] pos coll k)))))

;; we need to pass in the malli.core/tags function as an arg to avoid a cyclic reference
(defn catn-parser
([] (fn [_ _ pos coll k] (k {} pos coll)))
([kr & krs]
([tags] (fn [_ _ pos coll k] (k (tags {}) pos coll)))
([tags kr & krs]
(let [sp (reduce (fn [acc [tag r]]
(fn [driver regs m pos coll k]
(r driver regs pos coll
(fn [v pos coll] (acc driver regs (assoc m tag v) pos coll k)))))
(fn [_ _ m pos coll k] (k m pos coll))
(fn [_ _ m pos coll k] (k (tags m) pos coll))
(reverse (cons kr krs)))]
(fn [driver regs pos coll k] (sp driver regs {} pos coll k)))))

Expand All @@ -191,12 +192,13 @@
[] unparsers)
:malli.core/invalid))))

(defn catn-unparser [& unparsers]
;; cyclic ref avoidance here as well for malli.core/tags?
(defn catn-unparser [tags? & unparsers]
(let [unparsers (apply array-map (mapcat identity unparsers))]
(fn [m]
(if (and (map? m) (= (count m) (count unparsers)))
(if (and (tags? m) (= (count (:values m)) (count unparsers)))
(miu/-reduce-kv-valid (fn [coll tag unparser]
(if-some [kv (find m tag)]
(if-some [kv (find (:values m) tag)]
(miu/-map-valid #(into coll %) (unparser (val kv)))
:malli.core/invalid))
;; `m` is in hash order, so have to iterate over `unparsers` to restore seq order:
Expand Down Expand Up @@ -237,27 +239,29 @@
(park-validator! driver r regs pos coll k)))
rs))

(defn altn-parser [kr & krs]
(reduce (fn [r [tag r*]]
(let [r* (fmap-parser (fn [v] (miu/-tagged tag v)) r*)]
;; cyclic ref avoidance for malli.core/tag
(defn altn-parser [tag kr & krs]
(reduce (fn [r [t r*]]
(let [r* (fmap-parser (fn [v] (tag t v)) r*)]
(fn [driver regs pos coll k]
(park-validator! driver r* regs pos coll k) ; remember fallback
(park-validator! driver r regs pos coll k))))
(let [[tag r] kr]
(fmap-parser (fn [v] (miu/-tagged tag v)) r))
(let [[t r] kr]
(fmap-parser (fn [v] (tag t v)) r))
krs))

(defn alt-unparser [& unparsers]
(fn [x]
(reduce (fn [_ unparse] (miu/-map-valid reduced (unparse x)))
:malli.core/invalid unparsers)))

(defn altn-unparser [& unparsers]
;; cyclic ref avoidance for malli.core/tag?
(defn altn-unparser [tag? & unparsers]
(let [unparsers (into {} unparsers)]
(fn [x]
(if (miu/-tagged? x)
(if-some [kv (find unparsers (key x))]
((val kv) (val x))
(if (tag? x)
(if-some [kv (find unparsers (:key x))]
((val kv) (:value x))
:malli.core/invalid)
:malli.core/invalid))))

Expand Down
3 changes: 1 addition & 2 deletions src/malli/impl/util.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@

(def ^:const +max-size+ #?(:clj Long/MAX_VALUE, :cljs (.-MAX_VALUE js/Number)))

(defn -tagged [k v] #?(:clj (MapEntry. k v), :cljs (MapEntry. k v nil)))
(defn -tagged? [v] (instance? MapEntry v))
(defn -entry [k v] #?(:clj (MapEntry. k v), :cljs (MapEntry. k v nil)))

(defn -invalid? [x] #?(:clj (identical? x :malli.core/invalid), :cljs (keyword-identical? x :malli.core/invalid)))
(defn -map-valid [f v] (if (-invalid? v) v (f v)))
Expand Down
Loading
Loading