src/encode.bend

bend-toon-codec@0.1.0.0 · 281 lines · 36 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/num.bend as N
4import bend-toon-codec@0.1.0.0/src/text.bend as T
5import bend-toon-codec@0.1.0.0/src/rep.bend as R
6import bend-toon-codec@0.1.0.0/src/fold.bend as F
7
8# Encoder (spec sections 2, 3, 6 to 12), without key folding.
9
10type Delim is Data:
11  Comma{}
12  Tab{}
13  Pipe{}
14
15# Encoder options: spaces per indent level, the document delimiter, and key
16# folding (section 13.4): 0 is off, n of 2 or more folds chains of single-key
17# objects into dotted keys of at most n segments, 4294967295 folds whole chains.
18type EOpts is Data:
19  EOpts{indent: U32, delim: Delim, fold: U32}
20
21def defaults() -> EOpts:
22  EOpts{2, Comma{}, 0}
23
24# The delimiter as a code point.
25def code(+d: Delim) -> U32:
26  match d:
27    case Comma{}:
28      44
29    case Tab{}:
30      9
31    case Pipe{}:
32      124
33
34# What a header writes inside the brackets for this delimiter.
35def sym(+d: Delim) -> String:
36  match d:
37    case Comma{}:
38      ""
39    case Tab{}:
40      "\t"
41    case Pipe{}:
42      "|"
43
44def delim.text(+d: Delim) -> String:
45  SCon{Chr{code(d)}, SNil{}}
46
47# ---- primitives ------------------------------------------------------------------
48
49def bool.text(+b: Bool) -> String:
50  T.sel.s(b, "true", "false")
51
52def num.text(+m: Maybe<&2, String>) -> String:
53  match m:
54    case Some{t}:
55      t
56    case None{}:
57      "null"
58
59def is_prim(+j: J.Json) -> Bool:
60  match j:
61    case J.JNull{}:
62      True{}
63    case J.JBool{b}:
64      True{}
65    case J.JNum{t}:
66      True{}
67    case J.JStr{s}:
68      True{}
69    case _:
70      False{}
71
72# The text of a primitive, quoted as `d` (a code point) requires.
73def prim.text(+j: J.Json, +d: U32) -> String:
74  match j:
75    case J.JNull{}:
76      "null"
77    case J.JBool{b}:
78      bool.text(b)
79    case J.JNum{t}:
80      num.text(N.canon(t))
81    case J.JStr{s}:
82      T.enc.str(s, d)
83    case _:
84      ""
85
86# ---- array shape -----------------------------------------------------------------
87
88def count(+items: J.Json) -> U32:
89  match items:
90    case J.JCons{h, t}:
91      (1 + count(t) : U32)
92    case _:
93      0
94
95def all_prim(+items: J.Json) -> Bool:
96  match items:
97    case J.JCons{h, t}:
98      is_prim(h) && all_prim(t)
99    case _:
100      True{}
101
102# Primitives joined by the delimiter.
103def join.prims(+items: J.Json, +d: Delim) -> String:
104  match items:
105    case J.JCons{h, J.JNil{}}:
106      prim.text(h, code(d))
107    case J.JCons{h, t}:
108      prim.text(h, code(d)) ++ delim.text(d) ++ join.prims(t, d)
109    case _:
110      ""
111
112def fields.len(+fs: J.Json) -> U32:
113  match fs:
114    case J.JField{k, v, t}:
115      (1 + fields.len(t) : U32)
116    case _:
117      0
118
119def fields.prim(+fs: J.Json) -> Bool:
120  match fs:
121    case J.JField{k, v, t}:
122      is_prim(v) && fields.prim(t)
123    case _:
124      True{}
125
126def field.pick(+hit: Bool, +v: J.Json, +rest: J.Json) -> J.Json:
127  match hit:
128    case True{}:
129      v
130    case False{}:
131      rest
132
133def field.get(+k: String, +fs: J.Json) -> J.Json:
134  match fs:
135    case J.JField{k2, v, t}:
136      field.pick(String.eq(k, k2), v, field.get(k, t))
137    case _:
138      J.JNull{}
139
140def has_key(+k: String, +fs: J.Json) -> Bool:
141  match fs:
142    case J.JField{k2, v, t}:
143      String.eq(k, k2) || has_key(k, t)
144    case _:
145      False{}
146
147# The keys of an object.
148def keys.of(+fs: J.Json) -> List<&2, String>:
149  match fs:
150    case J.JField{k, v, t}:
151      Con{k, keys.of(t)}
152    case _:
153      Nil{}
154
155def has_all(+ks: List<&2, String>, +fs: J.Json) -> Bool:
156  match ks:
157    case Nil{}:
158      True{}
159    case Con{k, t}:
160      has_key(k, fs) && has_all(t, fs)
161
162def list.len(+ks: List<&2, String>) -> U32:
163  match ks:
164    case Nil{}:
165      0
166    case Con{k, t}:
167      (1 + list.len(t) : U32)
168
169# One element fits the header's keys: an object of primitives with exactly
170# those keys.
171def row.fits(+ks: List<&2, String>, +e: J.Json) -> Bool:
172  match e:
173    case J.JObj{fs}:
174      fields.prim(fs) && U32.is_eq(fields.len(fs), list.len(ks)) && has_all(ks, fs)
175    case _:
176      False{}
177
178def tab.ok(+ks: List<&2, String>, +items: J.Json) -> Bool:
179  match items:
180    case J.JCons{h, t}:
181      row.fits(ks, h) && tab.ok(ks, t)
182    case _:
183      True{}
184
185# Keys of the first element when it is a non-empty object, else none.
186def first.keys(+items: J.Json) -> List<&2, String>:
187  match items:
188    case J.JCons{J.JObj{fs}, t}:
189      keys.of(fs)
190    case _:
191      Nil{}
192
193def tabular.keys(+ks: List<&2, String>, +items: J.Json) -> Bool:
194  match ks:
195    case Nil{}:
196      False{}
197    case Con{k, t}:
198      tab.ok(Con{k, t}, items)
199
200def tabular(+items: J.Json) -> Bool:
201  tabular.keys(first.keys(items), items)
202
203def fields.text(+ks: List<&2, String>, +d: Delim) -> String:
204  match ks:
205    case Nil{}:
206      ""
207    case Con{k, Nil{}}:
208      T.enc.key(k)
209    case Con{k, t}:
210      T.enc.key(k) ++ delim.text(d) ++ fields.text(t, d)
211
212def row.cells(+ks: List<&2, String>, +fs: J.Json, +d: Delim) -> String:
213  match ks:
214    case Nil{}:
215      ""
216    case Con{k, Nil{}}:
217      prim.text(field.get(k, fs), code(d))
218    case Con{k, t}:
219      prim.text(field.get(k, fs), code(d)) ++ delim.text(d) ++ row.cells(t, fs, d)
220
221def row.of(+ks: List<&2, String>, +e: J.Json, +d: Delim) -> String:
222  match e:
223    case J.JObj{fs}:
224      row.cells(ks, fs, d)
225    case _:
226      ""
227
228def rows(+ks: List<&2, String>, +items: J.Json, +d: Delim) -> List<&2, R.Ln>:
229  match items:
230    case J.JCons{h, t}:
231      Con{R.Ln{1, row.of(ks, h, d)}, rows(ks, t, d)}
232    case _:
233      Nil{}
234
235def head(+items: J.Json, +d: Delim) -> String:
236  "[" ++ U32.show(count(items)) ++ sym(d) ++ "]"
237
238def tab.rep(+items: J.Json, +d: Delim) -> R.Rep:
239  R.RArr{head(items, d) ++ "{" ++ fields.text(first.keys(items), d) ++ "}:", rows(first.keys(items), items, d)}
240
241def arr.pick(+empty: Bool, +prims: Bool, +tab: Bool, +items: J.Json, +d: Delim, +ls: R.Rep) -> R.Rep:
242  match empty prims tab:
243    case True{} _ _:
244      R.REmpty{}
245    case False{} True{} _:
246      R.RArr{head(items, d) ++ ": " ++ join.prims(items, d), Nil{}}
247    case False{} False{} True{}:
248      tab.rep(items, d)
249    case False{} False{} False{}:
250      R.RArr{head(items, d) ++ ":", R.shift(R.lines.of(ls))}
251
252# An array's rep. `elem` is true for an element of another array, where the
253# tabular form is not available (section 9.4); `ls` is the encoding of the
254# elements as list items.
255def arr.rep(+elem: Bool, +items: J.Json, +d: Delim, +ls: R.Rep) -> R.Rep:
256  arr.pick(U32.is_zero(count(items)), all_prim(items), Bool.not(elem) && tabular(items), items, d, ls)
257
258# ---- the encoder -----------------------------------------------------------------
259
260# One def for values, object fields and list items. `elem` says the value is an
261# array element (no tabular form). `j` shrinks on every call.
262def enc(j: J.Json, +elem: Bool, +d: Delim) -> R.Rep:
263  match j:
264    case J.JArr{+items}:
265      arr.rep(elem, items, d, enc(items, True{}, d))
266    case J.JObj{fields}:
267      enc(fields, False{}, d)
268    case J.JNil{}:
269      R.RLines{Nil{}}
270    case J.JCons{h, t}:
271      R.RLines{R.cat(R.item(enc(h, True{}, d)), R.lines.of(enc(t, True{}, d)))}
272    case J.JField{k, v, t}:
273      R.RLines{R.cat(R.field(T.enc.key(k), enc(v, False{}, d)), R.lines.of(enc(t, False{}, d)))}
274    case _:
275      R.RPrim{prim.text(j, code(d))}
276
277# Encode a value as TOON text.
278def encode(j: J.Json, o: EOpts) -> String:
279  match o:
280    case EOpts{indent, d, fold}:
281      R.render(indent, R.root(enc(F.fold(j, fold), False{}, d)))