src/toml/toml.bend
ezx@1.9.0.0 · 341 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.9.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 chars gathered so far, last read first, spelled in the order they arrived
72def seg.chars(buf: List<&2, Char>, acc: String) -> String:
73 match buf:
74 case []:
75 acc
76 case hd <> tl:
77 seg.chars(tl, SCon{hd, acc})
78
79# the segment gathered so far, in order
80def seg.done(cur: List<&2, Char>) -> String:
81 seg.chars(cur, "")
82
83# a dot inside quotes is part of the segment; outside, it ends one
84def seg.dot(quoted: Bool, cur: List<&2, Char>, out: List<&2, String>) -> Split:
85 match quoted:
86 case True{}:
87 Split{True{}, '.' <> cur, out}
88 case False{}:
89 Split{False{}, [], seg.done(cur) <> out}
90
91# the state once a char has been classified
92def seg.go(st: Split, sym: Sym) -> Split:
93 Split{quoted, cur, out} = st
94 match sym:
95 case SQuote{}:
96 Split{Bool.not(quoted), cur, out}
97 case SDot{}:
98 seg.dot(quoted, cur, out)
99 case SKeep{ch}:
100 Split{quoted, ch <> cur, out}
101
102# one char of a header against the state
103def seg.step(st: Split, ch: Char) -> Split:
104 seg.go(st, seg.sym(ch))
105
106# the segment still being gathered, closed, and the rest put back in order
107def seg.fin(st: Split) -> List<&2, String>:
108 Split{_quoted, cur, out} = st
109 List.reverse(&2, String, seg.done(cur) <> out)
110
111# the walk one char at a time, written out rather than folded
112def seg.walk(cs: List<&2, Char>, st: Split) -> Split:
113 match cs:
114 case []:
115 st
116 case hd <> tl:
117 seg.walk(tl, seg.step(st, hd))
118
119# a header's segments, unquoted. `packages."0x0a".files` is three of them, and a
120# quoted segment may hold the dots a bare one may not.
121def segments(head: String) -> List<&2, String>:
122 seg.fin(seg.walk(String.to_list(head), Split{False{}, [], []}))
123
124# a string in quotes, which is how a segment or a key that is not bare is
125# written, and how every value is
126def quote(+text: String) -> String:
127 "\"" ++ text ++ "\""
128
129# a char a bare key may hold. `+ch` because it is asked four questions; a
130# predicate handed to a fold may not take one, so it is split out here.
131def bare.at(+ch: Char) -> Bool:
132 Bool.or(Char.is_alpha(ch), Bool.or(Char.is_digit(ch),
133 Bool.or(Char.is_eq(ch, '_'), Char.is_eq(ch, '-'))))
134
135# one step of the walk, with the head's answer in hand. `Bool.and` runs both
136# of its sides, so the first char that a bare key may not hold still cost a
137# walk of the rest of the key.
138def bare.all.step(here: Bool, rest: Unit -> Bool) -> Bool:
139 match here:
140 case True{}:
141 rest(Unit{})
142 case False{}:
143 False{}
144
145# whether every char of a key is one a bare key may hold. Base's `List.all`
146# applies an erased function parameter and so falls outside the termination
147# check; this walk is structural.
148def bare.all(cs: List<&2, Char>) -> Bool:
149 match cs:
150 case []:
151 True{}
152 case +hd <> tl:
153 bare.all.step(bare.at(hd), _u => bare.all(tl))
154
155# whether a key can be written without quotes
156def bare(+text: String) -> Bool:
157 Bool.and(Bool.not(String.is_empty(text)), bare.all(String.to_list(text)))
158
159# a key as it is written: bare when it can be, quoted when it must be
160def key(+text: String) -> String:
161 Bool.pick(String, bare(text), text, quote(text))
162
163# ---------------------------------------------------------------------------
164# a document as ez assembles it
165
166# one `key = "value"` line
167def render.pair(kv: Kv) -> String:
168 Kv{kk, vv} = kv
169 key(kk) ++ " = " ++ quote(vv) ++ "\n"
170
171# every pair of a section, in the order it holds them
172def render.pairs(ps: List<&2, Kv>) -> String:
173 match ps:
174 case []:
175 ""
176 case hd <> tl:
177 render.pair(hd) ++ render.pairs(tl)
178
179# one table: its header, then its pairs. The header is written as it is given,
180# so a caller that needs a quoted segment quotes it.
181def render.sect(sect: Sect) -> String:
182 Sect{nn, ps} = sect
183 "[" ++ nn ++ "]\n" ++ render.pairs(ps)
184
185# every table of a document
186def render.all(ss: List<&2, Sect>) -> List<&2, String>:
187 match ss:
188 case []:
189 []
190 case hd <> tl:
191 render.sect(hd) <> render.all(tl)
192
193# a document of tables, one blank line between them, as ez assembles it;
194# `normal` is what is written
195def render(ss: List<&2, Sect>) -> String:
196 String.join(render.all(ss), "\n")
197
198# a document written as eztoml writes it: read by eztoml, and rendered back
199def normal(text: String) -> String:
200 E.render(E.parse(text))
201
202# ---------------------------------------------------------------------------
203# a document read
204
205# a truth value as TOML writes it
206def flag.text(bit: Bool) -> String:
207 match bit:
208 case True{}:
209 "true"
210 case False{}:
211 "false"
212
213# a pair whose value is written as text: a string or a boolean, as ez writes
214# them; an integer as its digits. A table, an array or an inline table is not
215# a pair of the section.
216def pair.of(+name: String, val: E.Val) -> List<&2, Kv>:
217 match val:
218 case E.VStr{text}:
219 [Kv{name, text}]
220 case E.VSpan{src, nn}:
221 [Kv{name, E.string(E.VSpan{src, nn})}]
222 case E.VBool{bit}:
223 [Kv{name, flag.text(bit)}]
224 case E.VInt{_sign, digits}:
225 [Kv{name, digits}]
226 case _:
227 []
228
229# the pairs among a table's rows, in order
230def pairs.of(rows: List<&2, E.Val>) -> List<&2, Kv>:
231 match rows:
232 case []:
233 []
234 case E.VPair{name, val} <> tl:
235 List.append(&2, Kv, pair.of(name, val), pairs.of(tl))
236 case _hd <> tl:
237 pairs.of(tl)
238
239# whether a row is a table
240def row.table(row: E.Val) -> Bool:
241 match row:
242 case E.VPair{_name, E.VHead{_path, _rows}}:
243 True{}
244 case _:
245 False{}
246
247# the rest of the walk, when this row was not a table
248def has.table.step(here: Bool, rest: Unit -> Bool) -> Bool:
249 match here:
250 case True{}:
251 True{}
252 case False{}:
253 rest(Unit{})
254
255# whether a table's rows hold a table, stopping at the first
256def has.table(rows: List<&2, E.Val>) -> Bool:
257 match rows:
258 case []:
259 False{}
260 case hd <> tl:
261 has.table.step(row.table(hd), _u => has.table(tl))
262
263# a header as ez names it: its segments, each bare or quoted, joined by dots
264def head.of(segs: List<&2, String>) -> String:
265 match segs:
266 case []:
267 ""
268 case +hd <> []:
269 key(hd)
270 case +hd <> tl:
271 key(hd) ++ "." ++ head.of(tl)
272
273# a table's section, when it holds a pair or no table. A table that holds
274# only tables is the parent a dotted header implies, which ez never writes a
275# pair into; old ledgers and locks name no such table, so it is no section.
276# (segs arrive innermost first, as `tables` builds them; the header reads
277# them outermost first)
278def sect.put(skip: Bool, +ps: List<&2, Kv>, +segs: List<&2, String>) -> List<&2, Sect>:
279 match skip:
280 case True{}:
281 []
282 case False{}:
283 [Sect{head.of(List.reverse(&2, String, segs)), ps}]
284
285def sect.keep(+ps: List<&2, Kv>, parent: Bool, +segs: List<&2, String>) -> List<&2, Sect>:
286 sect.put(Bool.and(List.is_empty(&2, Kv, ps), parent), ps, segs)
287
288# a table's section, then the sections of the tables in it
289def table.sects(+segs: List<&2, String>, +sub: List<&2, E.Val>, inner: List<&2, Sect>) -> List<&2, Sect>:
290 List.append(&2, Sect, sect.keep(pairs.of(sub), has.table(sub), segs), inner)
291
292# the sections of every table among a table's rows, in document order: each
293# table's own section, then the tables in it, then the tables after it. The
294# path to the table is kept innermost first, so a level prepends its name.
295def tables(rows: List<&2, E.Val>, +segs: List<&2, String>) -> List<&2, Sect>:
296 match rows:
297 case []:
298 []
299 case E.VPair{name, E.VHead{_path, +sub}} <> tl:
300 +at : List<&2, String> = name <> segs
301 List.append(&2, Sect, table.sects(at, sub, tables(sub, at)), tables(tl, segs))
302 case _hd <> tl:
303 tables(tl, segs)
304
305# a document eztoml read, as sections
306def parse.of(+doc: E.Doc) -> Toml:
307 +rows = E.root(doc)
308 Toml{E.bad(doc), "", pairs.of(rows), tables(rows, [])}
309
310# a document read from TOML text: eztoml's reading of it, as sections
311def parse(text: String) -> Toml:
312 parse.of(E.parse(text))
313
314# ---------------------------------------------------------------------------
315# for a test or a report
316
317# a pair as `key=value`
318def show.kv(kv: Kv) -> String:
319 Kv{kk, vv} = kv
320 kk ++ "=" ++ vv
321
322# every pair of a section, shown
323def show.kvs(ps: List<&2, Kv>) -> List<&2, String>:
324 match ps:
325 case []:
326 []
327 case hd <> tl:
328 show.kv(hd) <> show.kvs(tl)
329
330# a section as `name key=value ..`
331def show.sect(sect: Sect) -> String:
332 Sect{nn, ps} = sect
333 String.join(nn <> show.kvs(ps), " ")
334
335# every section of a document, shown
336def show.sects(ss: List<&2, Sect>) -> List<&2, String>:
337 match ss:
338 case []:
339 []
340 case hd <> tl:
341 show.sect(hd) <> show.sects(tl)