Skip to content

Commit c0d9e3f

Browse files
authored
Merge pull request #1150 from metosin/tagged-record
Use custom Tag / Tags records (instead of MapEntry / Map) in parse output
2 parents 5941195 + 5e3d6b4 commit c0d9e3f

12 files changed

+213
-114
lines changed

CHANGELOG.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ We use [Break Versioning][breakver]. The version numbers follow a `<major>.<mino
1414

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

17-
##
17+
## UNRELEASED
1818

1919
* Docs: elaborate optional-keys and required-keys [#1117](https://github.com/metosin/malli/pull/1117)
20+
* **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)
21+
* See [Parsing](#parsing-values) and [Unparsing](#unparsing-values) for docs.
2022

2123
## 0.17.0 (2024-12-08)
2224

README.md

+36-12
Original file line numberDiff line numberDiff line change
@@ -2445,9 +2445,10 @@ Schemas can be used to parse values using `m/parse` and `m/parser`:
24452445
[:s string?]
24462446
[:b boolean?]]]]]
24472447
["-server" "foo" "-verbose" true "-user" "joe"])
2448-
;[{:prop "-server", :val [:s "foo"]}
2449-
; {:prop "-verbose", :val [:b true]}
2450-
; {:prop "-user", :val [:s "joe"]}]
2448+
;[#malli.core.Tags{:values {:prop "-server", :val #malli.core.Tag{:key :s, :value "foo"}}}
2449+
; #malli.core.Tags{:values {:prop "-verbose", :val #malli.core.Tag{:key :b, :value true}}}
2450+
; #malli.core.Tags{:values {:prop "-user", :val #malli.core.Tag{:key :s, :value "joe"}}}]
2451+
24512452
```
24522453

24532454
`m/parser` to create an optimized parser:
@@ -2471,13 +2472,25 @@ Schemas can be used to parse values using `m/parse` and `m/parser`:
24712472
(parse-hiccup
24722473
[:div {:class [:foo :bar]}
24732474
[:p "Hello, world of data"]])
2474-
;[:node
2475-
; {:name :div
2476-
; :props {:class [:foo :bar]}
2477-
; :children [[:node
2478-
; {:name :p
2479-
; :props nil
2480-
; :children [[:primitive [:text "Hello, world of data"]]]}]]}]
2475+
2476+
;#malli.core.Tag
2477+
;{:key :node,
2478+
; :value
2479+
; #malli.core.Tags
2480+
; {:values {:name :div,
2481+
; :props {:class [:foo :bar]},
2482+
; :children [#malli.core.Tag
2483+
; {:key :node,
2484+
; :value
2485+
; #malli.core.Tags
2486+
; {:values {:name :p,
2487+
; :props nil,
2488+
; :children [#malli.core.Tag
2489+
; {:key :primitive,
2490+
; :value
2491+
; #malli.core.Tag
2492+
; {:key :text,
2493+
; :value "Hello, world of data"}}]}}}]}}}
24812494
```
24822495

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

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

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

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

2524+
```clojure
2525+
(m/unparse [:orn [:name :string] [:id :int]]
2526+
(m/tagged :name "x"))
2527+
; => "x"
2528+
2529+
(m/unparse [:* [:catn [:name :string] [:id :int]]]
2530+
[(m/tags {:name "x" :id 1})
2531+
(m/tags {:name "y" :id 2})])
2532+
; => ["x" 1 "y" 2]
2533+
```
2534+
25112535
## Serializable functions
25122536

25132537
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

src/malli/core.cljc

+36-13
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,27 @@
148148
#?(:clj (defmethod print-method ::into-schema [v ^java.io.Writer w] (.write w (str "#IntoSchema{:type " (pr-str (-type ^IntoSchema v)) "}"))))
149149
#?(:clj (defmethod print-method ::schema [v ^java.io.Writer w] (.write w (pr-str (-form ^Schema v)))))
150150

151+
(defrecord Tag [key value])
152+
153+
(defn tag
154+
"A tagged value, used eg. for results of `parse` for `:orn` schemas."
155+
[key value] (->Tag key value))
156+
157+
(defn tag?
158+
"Is this a value constructed with `tag`?"
159+
[x] (instance? Tag x))
160+
161+
(defrecord Tags [values])
162+
163+
(defn tags
164+
"A collection of tagged values. `values` should be a map from tag to value.
165+
Used eg. for results of `parse` for `:catn` schemas."
166+
[values] (->Tags values))
167+
168+
(defn tags?
169+
"Is this a value constructed with `tags`?"
170+
[x] (instance? Tags x))
171+
151172
;;
152173
;; impl
153174
;;
@@ -385,7 +406,7 @@
385406
;;
386407

387408
(defn -simple-entry-parser [keyset children forms]
388-
(let [entries (map (fn [[k p s]] (miu/-tagged k (-val-schema s p))) children)]
409+
(let [entries (map (fn [[k p s]] (miu/-entry k (-val-schema s p))) children)]
389410
(reify EntryParser
390411
(-entry-keyset [_] keyset)
391412
(-entry-children [_] children)
@@ -606,7 +627,7 @@
606627
(reify EntryParser
607628
(-entry-keyset [_] keyset)
608629
(-entry-children [_] @children)
609-
(-entry-entries [_] (-vmap (fn [[k p s]] (miu/-tagged k (-val-schema s p))) @children))
630+
(-entry-entries [_] (-vmap (fn [[k p s]] (miu/-entry k (-val-schema s p))) @children))
610631
(-entry-forms [_] (->> @children (-vmap (fn [[k p v]] (if p [k p (-form v)] [k (-form v)]))))))))
611632

612633
(defn -from-entry-ast [parent ast options]
@@ -865,15 +886,15 @@
865886
(-parser [this]
866887
(let [parsers (-vmap (fn [[k _ c]]
867888
(let [c (-parser c)]
868-
(fn [x] (miu/-map-valid #(reduced (miu/-tagged k %)) (c x)))))
889+
(fn [x] (miu/-map-valid #(reduced (tag k %)) (c x)))))
869890
(-children this))]
870891
(fn [x] (reduce (fn [_ parser] (parser x)) x parsers))))
871892
(-unparser [this]
872893
(let [unparsers (into {} (map (fn [[k _ c]] [k (-unparser c)])) (-children this))]
873894
(fn [x]
874-
(if (miu/-tagged? x)
875-
(if-some [unparse (get unparsers (key x))]
876-
(unparse (val x))
895+
(if (tag? x)
896+
(if-some [unparse (get unparsers (:key x))]
897+
(unparse (:value x))
877898
::invalid)
878899
::invalid))))
879900
(-transformer [this transformer method options]
@@ -1011,6 +1032,8 @@
10111032
->parser (fn [this f]
10121033
(let [keyset (-entry-keyset (-entry-parser this))
10131034
default-parser (some-> @default-schema (f))
1035+
;; prevent unparsing :catn/:orn/etc parse results as maps
1036+
ok? #(and (pred? %) (not (tag? %)) (not (tags? %)))
10141037
parsers (cond->> (-vmap
10151038
(fn [[key {:keys [optional]} schema]]
10161039
(let [parser (f schema)]
@@ -1035,7 +1058,7 @@
10351058
(reduce
10361059
(fn [m k] (if (contains? keyset k) m (reduced (reduced ::invalid))))
10371060
m (keys m)))))]
1038-
(fn [x] (if (pred? x) (reduce (fn [m parser] (parser m)) x parsers) ::invalid))))]
1061+
(fn [x] (if (ok? x) (reduce (fn [m parser] (parser m)) x parsers) ::invalid))))]
10391062
^{:type ::schema}
10401063
(reify
10411064
AST
@@ -1645,12 +1668,12 @@
16451668
(let [->path (if (and (map? x) (keyword? dispatch)) #(conj % dispatch) identity)]
16461669
(conj acc (miu/-error (->path path) (->path in) this x ::invalid-dispatch-value)))))))
16471670
(-parser [_]
1648-
(let [parse (fn [k s] (let [p (-parser s)] (fn [x] (miu/-map-valid #(miu/-tagged k %) (p x)))))
1671+
(let [parse (fn [k s] (let [p (-parser s)] (fn [x] (miu/-map-valid #(tag k %) (p x)))))
16491672
find (finder (reduce-kv (fn [acc k s] (assoc acc k (parse k s))) {} @dispatch-map))]
16501673
(fn [x] (if-some [parser (find (dispatch x))] (parser x) ::invalid))))
16511674
(-unparser [_]
16521675
(let [unparsers (reduce-kv (fn [acc k s] (assoc acc k (-unparser s))) {} @dispatch-map)]
1653-
(fn [x] (if (miu/-tagged? x) (if-some [f (unparsers (key x))] (f (val x)) ::invalid) ::invalid))))
1676+
(fn [x] (if (tag? x) (if-some [f (unparsers (:key x))] (f (:value x)) ::invalid) ::invalid))))
16541677
(-transformer [this transformer method options]
16551678
;; FIXME: Probably should not use `dispatch`
16561679
;; Can't use `dispatch` as `x` might not be valid before it has been unparsed:
@@ -2683,15 +2706,15 @@
26832706
:catn (-sequence-entry-schema {:type :catn, :child-bounds {}, :keep false
26842707
:re-validator (fn [_ children] (apply re/cat-validator children))
26852708
:re-explainer (fn [_ children] (apply re/cat-explainer children))
2686-
:re-parser (fn [_ children] (apply re/catn-parser children))
2687-
:re-unparser (fn [_ children] (apply re/catn-unparser children))
2709+
:re-parser (fn [_ children] (apply re/catn-parser tags children))
2710+
:re-unparser (fn [_ children] (apply re/catn-unparser tags? children))
26882711
:re-transformer (fn [_ children] (apply re/cat-transformer children))
26892712
:re-min-max (fn [_ children] (reduce (partial -re-min-max +) {:min 0, :max 0} (-vmap last children)))})
26902713
:altn (-sequence-entry-schema {:type :altn, :child-bounds {:min 1}, :keep false
26912714
:re-validator (fn [_ children] (apply re/alt-validator children))
26922715
:re-explainer (fn [_ children] (apply re/alt-explainer children))
2693-
:re-parser (fn [_ children] (apply re/altn-parser children))
2694-
:re-unparser (fn [_ children] (apply re/altn-unparser children))
2716+
:re-parser (fn [_ children] (apply re/altn-parser tag children))
2717+
:re-unparser (fn [_ children] (apply re/altn-unparser tag? children))
26952718
:re-transformer (fn [_ children] (apply re/alt-transformer children))
26962719
:re-min-max (fn [_ children] (reduce -re-alt-min-max {:max 0} (-vmap last children)))})})
26972720

src/malli/destructure.cljc

+8-9
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
(ns malli.destructure
22
(:require [clojure.walk :as walk]
3-
[malli.core :as m]
4-
[malli.impl.util :as miu]))
3+
[malli.core :as m]))
54

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

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

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

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

99-
(defn -transform [{[k v] :arg schema :schema :as all} options rest]
98+
(defn -transform [{{{k :key v :value} :arg schema :schema :as all} :values} options rest]
10099
(cond (and schema rest) (let [s (-transform all options false)] (if (-any? s) schema s))
101100
schema schema
102101
(= :vec k) (-vector v options)
103102
(= :map k) (-map v options rest)
104103
rest [:* :any]
105104
:else :any))
106105

107-
(defn -schema [{:keys [elems rest]} options]
106+
(defn -schema [{{:keys [elems rest]} :values} options]
108107
(cond-> :cat
109108
(or (seq elems) rest) (vector)
110109
(seq elems) (into (map #(-transform % options false) elems))
111-
rest (conj (-transform (:arg rest) options true))))
110+
rest (conj (-transform (:arg (:values rest)) options true))))
112111

113112
(defn -unschematize [x]
114113
(walk/prewalk #(cond-> % (and (map? %) (:- %)) (dissoc :- :schema)) x))

src/malli/experimental.cljc

+6-5
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,14 @@
3636
(def Params (-schema false))
3737

3838
(c/defn -defn [schema args]
39-
(let [{:keys [name return doc arities] body-meta :meta :as parsed} (m/parse schema args)
39+
(let [{:keys [name return doc arities] body-meta :meta :as parsed} (:values (m/parse schema args))
40+
return (:values return)
4041
var-meta (meta name)
4142
_ (when (= ::m/invalid parsed) (m/-fail! ::parse-error {:schema schema, :args args}))
42-
parse (fn [{:keys [args] :as parsed}] (merge (md/parse args) parsed))
43+
parse (fn [parsed] (merge (md/parse (-> parsed :values :args)) (:values parsed)))
4344
->schema (fn [{:keys [schema]}] [:=> schema (:schema return :any)])
44-
single (= :single (key arities))
45-
parglists (if single (->> arities val parse vector) (->> arities val :arities (map parse)))
45+
single (= :single (:key arities))
46+
parglists (if single (->> arities :value parse vector) (->> arities :value :values :arities (map parse)))
4647
raw-arglists (map :raw-arglist parglists)
4748
schema (as-> (map ->schema parglists) $ (if single (first $) (into [:function] $)))
4849
bodies (map (fn [{:keys [arglist prepost body]}] `(~arglist ~prepost ~@body)) parglists)
@@ -60,7 +61,7 @@
6061
~@(some-> doc vector)
6162
~enriched-meta
6263
~@bodies
63-
~@(when-not single (some->> arities val :meta vector))))]
64+
~@(when-not single (some->> arities :value :meta vector))))]
6465
(m/=> ~name ~schema)
6566
defn#)))
6667

src/malli/impl/regex.cljc

+19-15
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,15 @@
172172
(reverse (cons r rs)))]
173173
(fn [driver regs pos coll k] (sp driver regs [] pos coll k)))))
174174

175+
;; we need to pass in the malli.core/tags function as an arg to avoid a cyclic reference
175176
(defn catn-parser
176-
([] (fn [_ _ pos coll k] (k {} pos coll)))
177-
([kr & krs]
177+
([tags] (fn [_ _ pos coll k] (k (tags {}) pos coll)))
178+
([tags kr & krs]
178179
(let [sp (reduce (fn [acc [tag r]]
179180
(fn [driver regs m pos coll k]
180181
(r driver regs pos coll
181182
(fn [v pos coll] (acc driver regs (assoc m tag v) pos coll k)))))
182-
(fn [_ _ m pos coll k] (k m pos coll))
183+
(fn [_ _ m pos coll k] (k (tags m) pos coll))
183184
(reverse (cons kr krs)))]
184185
(fn [driver regs pos coll k] (sp driver regs {} pos coll k)))))
185186

@@ -191,12 +192,13 @@
191192
[] unparsers)
192193
:malli.core/invalid))))
193194

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

240-
(defn altn-parser [kr & krs]
241-
(reduce (fn [r [tag r*]]
242-
(let [r* (fmap-parser (fn [v] (miu/-tagged tag v)) r*)]
242+
;; cyclic ref avoidance for malli.core/tag
243+
(defn altn-parser [tag kr & krs]
244+
(reduce (fn [r [t r*]]
245+
(let [r* (fmap-parser (fn [v] (tag t v)) r*)]
243246
(fn [driver regs pos coll k]
244247
(park-validator! driver r* regs pos coll k) ; remember fallback
245248
(park-validator! driver r regs pos coll k))))
246-
(let [[tag r] kr]
247-
(fmap-parser (fn [v] (miu/-tagged tag v)) r))
249+
(let [[t r] kr]
250+
(fmap-parser (fn [v] (tag t v)) r))
248251
krs))
249252

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

255-
(defn altn-unparser [& unparsers]
258+
;; cyclic ref avoidance for malli.core/tag?
259+
(defn altn-unparser [tag? & unparsers]
256260
(let [unparsers (into {} unparsers)]
257261
(fn [x]
258-
(if (miu/-tagged? x)
259-
(if-some [kv (find unparsers (key x))]
260-
((val kv) (val x))
262+
(if (tag? x)
263+
(if-some [kv (find unparsers (:key x))]
264+
((val kv) (:value x))
261265
:malli.core/invalid)
262266
:malli.core/invalid))))
263267

src/malli/impl/util.cljc

+1-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55

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

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

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

0 commit comments

Comments
 (0)