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)