src/toml/toml.bend
ezx@1.4.0.0 · 333 lines · 0 definitions
1# toml/toml: ez's view of a TOML document: sections of string pairs. ez's
2# ledger and lock are documents of tables of string (and one boolean) pairs,
3# and every command reads them as a list of sections, each a header's
4# segments and the pairs under it. Reading and writing TOML are eztoml's:
5# `parse` walks the document eztoml's `parse` reads into that list, and
6# `normal` writes a document through eztoml's `render`. Only eztoml's
7# interface, `main.bend`, is imported; that a document it renders reads back
8# as itself is its TOML-RT-1, trusted as EZ-TRUST-8.
9#
10# The rest is ez's own and pure: finding a key's value, a header's segments,
11# and the text of a section in the form ez assembles a document in before
12# eztoml writes it (`render`). That form is the TOML ez wrote before eztoml
13# 0.4, so an old ledger or lock and a new one read to the same sections.
14import Base
15import emerging-eztoml@0.8.0.0/main.bend as E
16
17# a key and the string written for it
18type Kv is Data:
19 Kv{key: String, val: String}
20
21# a section header and the pairs under it, in the order they were written
22type Sect is Data:
23 Sect{name: String, pairs: List<&2, Kv>}
24
25# a document read: the first error, "" when there is none, a name kept for
26# the shape callers match on (always ""), the pairs above the first header,
27# and every section that holds a pair, or holds no table, in document order
28type Toml is Data:
29 Toml{bad: String, name: String, pairs: List<&2, Kv>, done: List<&2, Sect>}
30
31# this pair's value when the key matches, otherwise whatever the rest of the
32# table gave
33def value.at(kv: Kv, rest: String, key: String) -> String:
34 Kv{kk, vv} = kv
35 Bool.pick(String, String.eq(kk, key), vv, rest)
36
37# the value written for a key in a table, or "" when it is absent. The scan
38# recurses first and picks after, so no recursive call hides inside a branch.
39def value(pairs: List<&2, Kv>, +key: String) -> String:
40 match pairs:
41 case []:
42 ""
43 case hd <> tl:
44 value.at(hd, value(tl, key), key)
45
46# the sections a parse found, in the order they were written
47def sects(doc: Toml) -> List<&2, Sect>:
48 Toml{_bad, _name, _pairs, done} = doc
49 done
50
51# ---------------------------------------------------------------------------
52# a header's segments
53
54# what a header's next character is: the quoting toggles, an unquoted dot cuts,
55# anything else is kept
56type Sym is Data:
57 SQuote{}
58 SDot{}
59 SKeep{ch: Char}
60
61# a header char classified. `+ch` because it is both asked about and kept.
62def seg.sym(+ch: Char) -> Sym:
63 Bool.pick(Sym, Char.is_eq(ch, '"'), SQuote{},
64 Bool.pick(Sym, Char.is_eq(ch, '.'), SDot{}, SKeep{ch}))
65
66# a header being cut apart: `quoted` is true inside quotes, `cur` is the segment
67# being gathered and `out` the finished ones, both reversed
68type Split is Data:
69 Split{quoted: Bool, cur: List<&2, Char>, out: List<&2, String>}
70
71# the segment gathered so far, in order
72def seg.done(cur: List<&2, Char>) -> String:
73 String.from_list(List.reverse(&2, Char, cur))
74
75# a dot inside quotes is part of the segment; outside, it ends one
76def seg.dot(quoted: Bool, cur: List<&2, Char>, out: List<&2, String>) -> Split:
77 match quoted:
78 case True{}:
79 Split{True{}, '.' <> cur, out}
80 case False{}:
81 Split{False{}, [], seg.done(cur) <> out}
82
83# the state once a char has been classified
84def seg.go(st: Split, sym: Sym) -> Split:
85 Split{quoted, cur, out} = st
86 match sym:
87 case SQuote{}:
88 Split{Bool.not(quoted), cur, out}
89 case SDot{}:
90 seg.dot(quoted, cur, out)
91 case SKeep{ch}:
92 Split{quoted, ch <> cur, out}
93
94# one char of a header against the state
95def seg.step(st: Split, ch: Char) -> Split:
96 seg.go(st, seg.sym(ch))
97
98# the segment still being gathered, closed, and the rest put back in order
99def seg.fin(st: Split) -> List<&2, String>:
100 Split{_quoted, cur, out} = st
101 List.reverse(&2, String, seg.done(cur) <> out)
102
103# the walk one char at a time, written out rather than folded
104def seg.walk(cs: List<&2, Char>, st: Split) -> Split:
105 match cs:
106 case []:
107 st
108 case hd <> tl:
109 seg.walk(tl, seg.step(st, hd))
110
111# a header's segments, unquoted. `packages."0x0a".files` is three of them, and a
112# quoted segment may hold the dots a bare one may not.
113def segments(head: String) -> List<&2, String>:
114 seg.fin(seg.walk(String.to_list(head), Split{False{}, [], []}))
115
116# a string in quotes, which is how a segment or a key that is not bare is
117# written, and how every value is
118def quote(+text: String) -> String:
119 "\"" ++ text ++ "\""
120
121# a char a bare key may hold. `+ch` because it is asked four questions; a
122# predicate handed to a fold may not take one, so it is split out here.
123def bare.at(+ch: Char) -> Bool:
124 Bool.or(Char.is_alpha(ch), Bool.or(Char.is_digit(ch),
125 Bool.or(Char.is_eq(ch, '_'), Char.is_eq(ch, '-'))))
126
127# one step of the walk, with the head's answer in hand. `Bool.and` runs both
128# of its sides, so the first char that a bare key may not hold still cost a
129# walk of the rest of the key.
130def bare.all.step(here: Bool, rest: Unit -> Bool) -> Bool:
131 match here:
132 case True{}:
133 rest(Unit{})
134 case False{}:
135 False{}
136
137# whether every char of a key is one a bare key may hold. Base's `List.all`
138# applies an erased function parameter and so falls outside the termination
139# check; this walk is structural.
140def bare.all(cs: List<&2, Char>) -> Bool:
141 match cs:
142 case []:
143 True{}
144 case +hd <> tl:
145 bare.all.step(bare.at(hd), _u => bare.all(tl))
146
147# whether a key can be written without quotes
148def bare(+text: String) -> Bool:
149 Bool.and(Bool.not(String.is_empty(text)), bare.all(String.to_list(text)))
150
151# a key as it is written: bare when it can be, quoted when it must be
152def key(+text: String) -> String:
153 Bool.pick(String, bare(text), text, quote(text))
154
155# ---------------------------------------------------------------------------
156# a document as ez assembles it
157
158# one `key = "value"` line
159def render.pair(kv: Kv) -> String:
160 Kv{kk, vv} = kv
161 key(kk) ++ " = " ++ quote(vv) ++ "\n"
162
163# every pair of a section, in the order it holds them
164def render.pairs(ps: List<&2, Kv>) -> String:
165 match ps:
166 case []:
167 ""
168 case hd <> tl:
169 render.pair(hd) ++ render.pairs(tl)
170
171# one table: its header, then its pairs. The header is written as it is given,
172# so a caller that needs a quoted segment quotes it.
173def render.sect(sect: Sect) -> String:
174 Sect{nn, ps} = sect
175 "[" ++ nn ++ "]\n" ++ render.pairs(ps)
176
177# every table of a document
178def render.all(ss: List<&2, Sect>) -> List<&2, String>:
179 match ss:
180 case []:
181 []
182 case hd <> tl:
183 render.sect(hd) <> render.all(tl)
184
185# a document of tables, one blank line between them, as ez assembles it;
186# `normal` is what is written
187def render(ss: List<&2, Sect>) -> String:
188 String.join(render.all(ss), "\n")
189
190# a document written as eztoml writes it: read by eztoml, and rendered back
191def normal(text: String) -> String:
192 E.render(E.parse(text))
193
194# ---------------------------------------------------------------------------
195# a document read
196
197# a truth value as TOML writes it
198def flag.text(bit: Bool) -> String:
199 match bit:
200 case True{}:
201 "true"
202 case False{}:
203 "false"
204
205# a pair whose value is written as text: a string or a boolean, as ez writes
206# them; an integer as its digits. A table, an array or an inline table is not
207# a pair of the section.
208def pair.of(+name: String, val: E.Val) -> List<&2, Kv>:
209 match val:
210 case E.VStr{text}:
211 [Kv{name, text}]
212 case E.VSpan{src, nn}:
213 [Kv{name, E.string(E.VSpan{src, nn})}]
214 case E.VBool{bit}:
215 [Kv{name, flag.text(bit)}]
216 case E.VInt{_sign, digits}:
217 [Kv{name, digits}]
218 case _:
219 []
220
221# the pairs among a table's rows, in order
222def pairs.of(rows: List<&2, E.Val>) -> List<&2, Kv>:
223 match rows:
224 case []:
225 []
226 case E.VPair{name, val} <> tl:
227 List.append(&2, Kv, pair.of(name, val), pairs.of(tl))
228 case _hd <> tl:
229 pairs.of(tl)
230
231# whether a row is a table
232def row.table(row: E.Val) -> Bool:
233 match row:
234 case E.VPair{_name, E.VHead{_path, _rows}}:
235 True{}
236 case _:
237 False{}
238
239# the rest of the walk, when this row was not a table
240def has.table.step(here: Bool, rest: Unit -> Bool) -> Bool:
241 match here:
242 case True{}:
243 True{}
244 case False{}:
245 rest(Unit{})
246
247# whether a table's rows hold a table, stopping at the first
248def has.table(rows: List<&2, E.Val>) -> Bool:
249 match rows:
250 case []:
251 False{}
252 case hd <> tl:
253 has.table.step(row.table(hd), _u => has.table(tl))
254
255# a header as ez names it: its segments, each bare or quoted, joined by dots
256def head.of(segs: List<&2, String>) -> String:
257 match segs:
258 case []:
259 ""
260 case +hd <> []:
261 key(hd)
262 case +hd <> tl:
263 key(hd) ++ "." ++ head.of(tl)
264
265# a table's section, when it holds a pair or no table. A table that holds
266# only tables is the parent a dotted header implies, which ez never writes a
267# pair into; old ledgers and locks name no such table, so it is no section.
268# (segs arrive innermost first, as `tables` builds them; the header reads
269# them outermost first)
270def sect.put(skip: Bool, +ps: List<&2, Kv>, +segs: List<&2, String>) -> List<&2, Sect>:
271 match skip:
272 case True{}:
273 []
274 case False{}:
275 [Sect{head.of(List.reverse(&2, String, segs)), ps}]
276
277def sect.keep(+ps: List<&2, Kv>, parent: Bool, +segs: List<&2, String>) -> List<&2, Sect>:
278 sect.put(Bool.and(List.is_empty(&2, Kv, ps), parent), ps, segs)
279
280# a table's section, then the sections of the tables in it
281def table.sects(+segs: List<&2, String>, +sub: List<&2, E.Val>, inner: List<&2, Sect>) -> List<&2, Sect>:
282 List.append(&2, Sect, sect.keep(pairs.of(sub), has.table(sub), segs), inner)
283
284# the sections of every table among a table's rows, in document order: each
285# table's own section, then the tables in it, then the tables after it. The
286# path to the table is kept innermost first, so a level prepends its name.
287def tables(rows: List<&2, E.Val>, +segs: List<&2, String>) -> List<&2, Sect>:
288 match rows:
289 case []:
290 []
291 case E.VPair{name, E.VHead{_path, +sub}} <> tl:
292 +at : List<&2, String> = name <> segs
293 List.append(&2, Sect, table.sects(at, sub, tables(sub, at)), tables(tl, segs))
294 case _hd <> tl:
295 tables(tl, segs)
296
297# a document eztoml read, as sections
298def parse.of(+doc: E.Doc) -> Toml:
299 +rows = E.root(doc)
300 Toml{E.bad(doc), "", pairs.of(rows), tables(rows, [])}
301
302# a document read from TOML text: eztoml's reading of it, as sections
303def parse(text: String) -> Toml:
304 parse.of(E.parse(text))
305
306# ---------------------------------------------------------------------------
307# for a test or a report
308
309# a pair as `key=value`
310def show.kv(kv: Kv) -> String:
311 Kv{kk, vv} = kv
312 kk ++ "=" ++ vv
313
314# every pair of a section, shown
315def show.kvs(ps: List<&2, Kv>) -> List<&2, String>:
316 match ps:
317 case []:
318 []
319 case hd <> tl:
320 show.kv(hd) <> show.kvs(tl)
321
322# a section as `name key=value ..`
323def show.sect(sect: Sect) -> String:
324 Sect{nn, ps} = sect
325 String.join(nn <> show.kvs(ps), " ")
326
327# every section of a document, shown
328def show.sects(ss: List<&2, Sect>) -> List<&2, String>:
329 match ss:
330 case []:
331 []
332 case hd <> tl:
333 show.sect(hd) <> show.sects(tl)