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)