src/expand.bend

bend-toon-codec@0.1.0.0 · 216 lines · 24 definitions

1import Base
2import bend-toon-codec@0.1.0.0/src/json.bend as J
3import bend-toon-codec@0.1.0.0/src/split.bend as S
4import bend-toon-codec@0.1.0.0/src/head.bend as Hd
5import bend-toon-codec@0.1.0.0/src/fold.bend as K
6
7# Path expansion (spec section 13.4, decoder, mode "safe"): an unquoted key
8# `a.b.c` whose segments are all identifiers becomes the nested object
9# `{a: {b: {c: ..}}}`, and overlapping paths merge. A quoted key is literal.
10#
11# It is a pass over the decoded value, as in the reference decoder. Objects
12# arrive with their quoted keys marked (head.bend), arrays and values are
13# expanded first, then each field is flattened to leaf assignments
14# (path, leaf) which are inserted in document order into the object being
15# built. A conflict (a leaf where an object must be, an object where a leaf
16# is, two leaves at one path) is an error in strict mode and last-write-wins
17# otherwise.
18
19# ---- the path of a key -----------------------------------------------------------------
20
21def all_ident(+ks: List<&2, String>) -> Bool:
22  match ks:
23    case Nil{}:
24      True{}
25    case Con{k, t}:
26      K.ident(k) && all_ident(t)
27
28# A bare key with a dot and only identifier segments expands.
29def eligible(+k: String) -> Bool:
30  K.has_dot(k) && all_ident(S.split(k, 46))
31
32def segs.bare(+e: Bool, +k: String) -> List<&2, String>:
33  match e:
34    case True{}:
35      S.split(k, 46)
36    case False{}:
37      Con{k, Nil{}}
38
39def segs.pick(+quoted: Bool, +k: String) -> List<&2, String>:
40  match quoted:
41    case True{}:
42      Con{Hd.unmark(k), Nil{}}
43    case False{}:
44      segs.bare(eligible(k), k)
45
46def segs_of(+k: String) -> List<&2, String>:
47  segs.pick(Hd.marked(k), k)
48
49def snoc(+ks: List<&2, String>, +k: String) -> List<&2, String>:
50  match ks:
51    case Nil{}:
52      Con{k, Nil{}}
53    case Con{h, t}:
54      Con{h, snoc(t, k)}
55
56# ---- inserting a leaf at a path ----------------------------------------------------------
57
58# The nested objects for a path ending in `leaf`.
59def build(+segs: List<&2, String>, +leaf: J.Json) -> J.Json:
60  match segs:
61    case Nil{}:
62      leaf
63    case Con{s, rest}:
64      J.JObj{J.JField{s, build(rest, leaf), J.JNil{}}}
65
66def conflict(+strict: Bool, +segs: List<&2, String>, +leaf: J.Json) -> Result<&2, &2, String, J.Json>:
67  match strict:
68    case True{}:
69      Fail{"expansion conflict"}
70    case False{}:
71      Done{build(segs, leaf)}
72
73def empty_obj(+j: J.Json) -> Bool:
74  match j:
75    case J.JObj{J.JNil{}}:
76      True{}
77    case _:
78      False{}
79
80# An existing object met by the end of a path: only an empty object leaf agrees.
81def same.pick(+ok: Bool, +strict: Bool, +e: J.Json, +leaf: J.Json) -> Result<&2, &2, String, J.Json>:
82  match ok:
83    case True{}:
84      Done{e}
85    case False{}:
86      conflict(strict, Nil{}, leaf)
87
88def wrap.obj(+r: Result<&2, &2, String, J.Json>) -> Result<&2, &2, String, J.Json>:
89  match r:
90    case Done{fs}:
91      Done{J.JObj{fs}}
92    case Fail{e}:
93      Fail{e}
94
95# A cell of a field chain: replace the value when the key is the path's head,
96# else look further along the chain.
97def cell(+hit: Bool, +k: String, +ev: J.Json, +t: J.Json, +rv: Result<&2, &2, String, J.Json>, +rt: Result<&2, &2, String, J.Json>) -> Result<&2, &2, String, J.Json>:
98  match hit rv rt:
99    case True{} Done{v} _:
100      Done{J.JField{k, v, t}}
101    case True{} Fail{e} _:
102      Fail{e}
103    case False{} _ Done{t2}:
104      Done{J.JField{k, ev, t2}}
105    case False{} _ Fail{e}:
106      Fail{e}
107
108# Insert `leaf` at `segs` inside `e`: an object value, a field chain, or
109# whatever sits at the path (then it conflicts unless the path has ended).
110def ins(+e: J.Json, +segs: List<&2, String>, +leaf: J.Json, +strict: Bool) -> Result<&2, &2, String, J.Json>:
111  match e segs:
112    case J.JObj{efs} Nil{}:
113      same.pick(empty_obj(leaf), strict, e, leaf)
114    case J.JObj{efs} Con{s, rest}:
115      wrap.obj(ins(efs, segs, leaf, strict))
116    case J.JField{k, ev, t} Con{s, rest}:
117      cell(Hd.keq(k, s), k, ev, t, ins(ev, rest, leaf, strict), ins(t, segs, leaf, strict))
118    case J.JNil{} Con{s, rest}:
119      Done{J.JField{s, build(rest, leaf), J.JNil{}}}
120    case J.JField{k, ev, t} Nil{}:
121      Done{e}
122    case J.JNil{} Nil{}:
123      Done{e}
124    case _ _:
125      conflict(strict, segs, leaf)
126
127def ins.r(+acc: Result<&2, &2, String, J.Json>, +segs: List<&2, String>, +leaf: J.Json, +strict: Bool) -> Result<&2, &2, String, J.Json>:
128  match acc:
129    case Done{a}:
130      ins(a, segs, leaf, strict)
131    case Fail{e}:
132      Fail{e}
133
134# Flatten an expanded value at `segs` into leaf assignments, inserted in order.
135def fl.pick(+empty: Bool, +j: J.Json, +segs: List<&2, String>, +acc: Result<&2, &2, String, J.Json>, +strict: Bool, +r: Result<&2, &2, String, J.Json>) -> Result<&2, &2, String, J.Json>:
136  match empty:
137    case True{}:
138      ins.r(acc, segs, j, strict)
139    case False{}:
140      r
141
142def fl(+j: J.Json, +segs: List<&2, String>, +acc: Result<&2, &2, String, J.Json>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
143  match j:
144    case J.JObj{fs}:
145      fl.pick(empty_obj(j), j, segs, acc, strict, fl(fs, segs, acc, strict))
146    case J.JField{k, v, t}:
147      fl(t, segs, fl(v, snoc(segs, k), acc, strict), strict)
148    case J.JNil{}:
149      acc
150    case _:
151      ins.r(acc, segs, j, strict)
152
153# ---- the pass ----------------------------------------------------------------------------
154
155def init() -> Result<&2, &2, String, J.Json>:
156  Done{J.JObj{J.JNil{}}}
157
158def fl.r(+vr: Result<&2, &2, String, J.Json>, +segs: List<&2, String>, +acc: Result<&2, &2, String, J.Json>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
159  match vr:
160    case Done{v}:
161      fl(v, segs, acc, strict)
162    case Fail{e}:
163      Fail{e}
164
165def arr.r(+r: Result<&2, &2, String, J.Json>) -> Result<&2, &2, String, J.Json>:
166  match r:
167    case Done{items}:
168      Done{J.JArr{items}}
169    case Fail{e}:
170      Fail{e}
171
172def cons.r(+h: Result<&2, &2, String, J.Json>, +t: Result<&2, &2, String, J.Json>) -> Result<&2, &2, String, J.Json>:
173  match h t:
174    case Done{x} Done{y}:
175      Done{J.JCons{x, y}}
176    case Fail{e} _:
177      Fail{e}
178    case _ Fail{e}:
179      Fail{e}
180
181def nil() -> Result<&2, &2, String, J.Json>:
182  Done{J.JNil{}}
183
184# `acc` is the object under construction while walking a field chain, and
185# Done{JNil} while walking an array's elements.
186def ex(+j: J.Json, +acc: Result<&2, &2, String, J.Json>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
187  match j:
188    case J.JObj{fs}:
189      ex(fs, init(), strict)
190    case J.JField{k, v, t}:
191      ex(t, fl.r(ex(v, init(), strict), segs_of(k), acc, strict), strict)
192    case J.JNil{}:
193      acc
194    case J.JArr{items}:
195      arr.r(ex(items, nil(), strict))
196    case J.JCons{h, t}:
197      cons.r(ex(h, init(), strict), ex(t, nil(), strict))
198    case _:
199      Done{j}
200
201# Remove the quoted-key markers (path expansion off).
202def strip(+j: J.Json) -> J.Json:
203  match j:
204    case J.JObj{x}:
205      J.JObj{strip(x)}
206    case J.JArr{x}:
207      J.JArr{strip(x)}
208    case J.JCons{h, t}:
209      J.JCons{strip(h), strip(t)}
210    case J.JField{k, v, t}:
211      J.JField{Hd.unmark(k), strip(v), strip(t)}
212    case _:
213      j
214
215def expand(+j: J.Json, +strict: Bool) -> Result<&2, &2, String, J.Json>:
216  ex(j, init(), strict)