src/toml/toml.bend source
src/toml/toml.bend on the hub · documented module
# toml/toml: ez's view of a TOML document: sections of string pairs. ez's# ledger and lock are documents of tables of string (and one boolean) pairs,# and every command reads them as a list of sections, each a header's# segments and the pairs under it. Reading and writing TOML are eztoml's:# `parse` walks the document eztoml's `parse` reads into that list, and# `normal` writes a document through eztoml's `render`. Only eztoml's# interface, `main.bend`, is imported; that a document it renders reads back# as itself is its TOML-RT-1, trusted as EZ-TRUST-8.## The rest is ez's own and pure: finding a key's value, a header's segments,# and the text of a section in the form ez assembles a document in before# eztoml writes it (`render`). That form is the TOML ez wrote before eztoml# 0.4, so an old ledger or lock and a new one read to the same sections.import Baseimport 0x8fb95168b7719a8faec16af7ee47b246/main.bend as E# a key and the string written for ittype Kv is Data: Kv{key: String, val: String}# a section header and the pairs under it, in the order they were writtentype Sect is Data: Sect{name: String, pairs: List<&2, Kv>}# a document read: the first error, "" when there is none, a name kept for# the shape callers match on (always ""), the pairs above the first header,# and every section that holds a pair, or holds no table, in document ordertype Toml is Data: Toml{bad: String, name: String, pairs: List<&2, Kv>, done: List<&2, Sect>}# this pair's value when the key matches, otherwise whatever the rest of the# table gavedef value.at(kv: Kv, rest: String, key: String) -> String: Kv{kk, vv} = kv Bool.pick(String, String.eq(kk, key), vv, rest)# the value written for a key in a table, or "" when it is absent. The scan# recurses first and picks after, so no recursive call hides inside a branch.def value(pairs: List<&2, Kv>, +key: String) -> String: match pairs: case []: "" case hd <> tl: value.at(hd, value(tl, key), key)# the sections a parse found, in the order they were writtendef sects(doc: Toml) -> List<&2, Sect>: Toml{_bad, _name, _pairs, done} = doc done# ---------------------------------------------------------------------------# a header's segments# what a header's next character is: the quoting toggles, an unquoted dot cuts,# anything else is kepttype Sym is Data: SQuote{} SDot{} SKeep{ch: Char}# a header char classified. `+ch` because it is both asked about and kept.def seg.sym(+ch: Char) -> Sym: Bool.pick(Sym, Char.is_eq(ch, '"'), SQuote{}, Bool.pick(Sym, Char.is_eq(ch, '.'), SDot{}, SKeep{ch}))# a header being cut apart: `quoted` is true inside quotes, `cur` is the segment# being gathered and `out` the finished ones, both reversedtype Split is Data: Split{quoted: Bool, cur: List<&2, Char>, out: List<&2, String>}# the segment gathered so far, in orderdef seg.done(cur: List<&2, Char>) -> String: String.from_list(List.reverse(&2, Char, cur))# a dot inside quotes is part of the segment; outside, it ends onedef seg.dot(quoted: Bool, cur: List<&2, Char>, out: List<&2, String>) -> Split: match quoted: case True{}: Split{True{}, '.' <> cur, out} case False{}: Split{False{}, [], seg.done(cur) <> out}# the state once a char has been classifieddef seg.go(st: Split, sym: Sym) -> Split: Split{quoted, cur, out} = st match sym: case SQuote{}: Split{Bool.not(quoted), cur, out} case SDot{}: seg.dot(quoted, cur, out) case SKeep{ch}: Split{quoted, ch <> cur, out}# one char of a header against the statedef seg.step(st: Split, ch: Char) -> Split: seg.go(st, seg.sym(ch))# the segment still being gathered, closed, and the rest put back in orderdef seg.fin(st: Split) -> List<&2, String>: Split{_quoted, cur, out} = st List.reverse(&2, String, seg.done(cur) <> out)# the walk one char at a time, written out rather than foldeddef seg.walk(cs: List<&2, Char>, st: Split) -> Split: match cs: case []: st case hd <> tl: seg.walk(tl, seg.step(st, hd))# a header's segments, unquoted. `packages."0x0a".files` is three of them, and a# quoted segment may hold the dots a bare one may not.def segments(head: String) -> List<&2, String>: seg.fin(seg.walk(String.to_list(head), Split{False{}, [], []}))# a string in quotes, which is how a segment or a key that is not bare is# written, and how every value isdef quote(+text: String) -> String: "\"" ++ text ++ "\""# a char a bare key may hold. `+ch` because it is asked four questions; a# predicate handed to a fold may not take one, so it is split out here.def bare.at(+ch: Char) -> Bool: Bool.or(Char.is_alpha(ch), Bool.or(Char.is_digit(ch), Bool.or(Char.is_eq(ch, '_'), Char.is_eq(ch, '-'))))# one step of the walk, with the head's answer in hand. `Bool.and` runs both# of its sides, so the first char that a bare key may not hold still cost a# walk of the rest of the key.def bare.all.step(here: Bool, rest: Unit -> Bool) -> Bool: match here: case True{}: rest(Unit{}) case False{}: False{}# whether every char of a key is one a bare key may hold. Base's `List.all`# applies an erased function parameter and so falls outside the termination# check; this walk is structural.def bare.all(cs: List<&2, Char>) -> Bool: match cs: case []: True{} case +hd <> tl: bare.all.step(bare.at(hd), _u => bare.all(tl))# whether a key can be written without quotesdef bare(+text: String) -> Bool: Bool.and(Bool.not(String.is_empty(text)), bare.all(String.to_list(text)))# a key as it is written: bare when it can be, quoted when it must bedef key(+text: String) -> String: Bool.pick(String, bare(text), text, quote(text))# ---------------------------------------------------------------------------# a document as ez assembles it# one `key = "value"` linedef render.pair(kv: Kv) -> String: Kv{kk, vv} = kv key(kk) ++ " = " ++ quote(vv) ++ "\n"# every pair of a section, in the order it holds themdef render.pairs(ps: List<&2, Kv>) -> String: match ps: case []: "" case hd <> tl: render.pair(hd) ++ render.pairs(tl)# one table: its header, then its pairs. The header is written as it is given,# so a caller that needs a quoted segment quotes it.def render.sect(sect: Sect) -> String: Sect{nn, ps} = sect "[" ++ nn ++ "]\n" ++ render.pairs(ps)# every table of a documentdef render.all(ss: List<&2, Sect>) -> List<&2, String>: match ss: case []: [] case hd <> tl: render.sect(hd) <> render.all(tl)# a document of tables, one blank line between them, as ez assembles it;# `normal` is what is writtendef render(ss: List<&2, Sect>) -> String: String.join(render.all(ss), "\n")# a document written as eztoml writes it: read by eztoml, and rendered backdef normal(text: String) -> String: E.render(E.parse(text))# ---------------------------------------------------------------------------# a document read# a truth value as TOML writes itdef flag.text(bit: Bool) -> String: match bit: case True{}: "true" case False{}: "false"# a pair whose value is written as text: a string or a boolean, as ez writes# them; an integer as its digits. A table, an array or an inline table is not# a pair of the section.def pair.of(+name: String, val: E.Val) -> List<&2, Kv>: match val: case E.VStr{text}: [Kv{name, text}] case E.VSpan{src, nn}: [Kv{name, E.string(E.VSpan{src, nn})}] case E.VBool{bit}: [Kv{name, flag.text(bit)}] case E.VInt{_sign, digits}: [Kv{name, digits}] case _: []# the pairs among a table's rows, in orderdef pairs.of(rows: List<&2, E.Val>) -> List<&2, Kv>: match rows: case []: [] case E.VPair{name, val} <> tl: List.append(&2, Kv, pair.of(name, val), pairs.of(tl)) case _hd <> tl: pairs.of(tl)# whether a row is a tabledef row.table(row: E.Val) -> Bool: match row: case E.VPair{_name, E.VHead{_path, _rows}}: True{} case _: False{}# the rest of the walk, when this row was not a tabledef has.table.step(here: Bool, rest: Unit -> Bool) -> Bool: match here: case True{}: True{} case False{}: rest(Unit{})# whether a table's rows hold a table, stopping at the firstdef has.table(rows: List<&2, E.Val>) -> Bool: match rows: case []: False{} case hd <> tl: has.table.step(row.table(hd), _u => has.table(tl))# a header as ez names it: its segments, each bare or quoted, joined by dotsdef head.of(segs: List<&2, String>) -> String: match segs: case []: "" case +hd <> []: key(hd) case +hd <> tl: key(hd) ++ "." ++ head.of(tl)# a table's section, when it holds a pair or no table. A table that holds# only tables is the parent a dotted header implies, which ez never writes a# pair into; old ledgers and locks name no such table, so it is no section.# (segs arrive innermost first, as `tables` builds them; the header reads# them outermost first)def sect.put(skip: Bool, +ps: List<&2, Kv>, +segs: List<&2, String>) -> List<&2, Sect>: match skip: case True{}: [] case False{}: [Sect{head.of(List.reverse(&2, String, segs)), ps}]def sect.keep(+ps: List<&2, Kv>, parent: Bool, +segs: List<&2, String>) -> List<&2, Sect>: sect.put(Bool.and(List.is_empty(&2, Kv, ps), parent), ps, segs)# a table's section, then the sections of the tables in itdef table.sects(+segs: List<&2, String>, +sub: List<&2, E.Val>, inner: List<&2, Sect>) -> List<&2, Sect>: List.append(&2, Sect, sect.keep(pairs.of(sub), has.table(sub), segs), inner)# the sections of every table among a table's rows, in document order: each# table's own section, then the tables in it, then the tables after it. The# path to the table is kept innermost first, so a level prepends its name.def tables(rows: List<&2, E.Val>, +segs: List<&2, String>) -> List<&2, Sect>: match rows: case []: [] case E.VPair{name, E.VHead{_path, +sub}} <> tl: +at : List<&2, String> = name <> segs List.append(&2, Sect, table.sects(at, sub, tables(sub, at)), tables(tl, segs)) case _hd <> tl: tables(tl, segs)# a document eztoml read, as sectionsdef parse.of(+doc: E.Doc) -> Toml: +rows = E.root(doc) Toml{E.bad(doc), "", pairs.of(rows), tables(rows, [])}# a document read from TOML text: eztoml's reading of it, as sectionsdef parse(text: String) -> Toml: parse.of(E.parse(text))# ---------------------------------------------------------------------------# for a test or a report# a pair as `key=value`def show.kv(kv: Kv) -> String: Kv{kk, vv} = kv kk ++ "=" ++ vv# every pair of a section, showndef show.kvs(ps: List<&2, Kv>) -> List<&2, String>: match ps: case []: [] case hd <> tl: show.kv(hd) <> show.kvs(tl)# a section as `name key=value ..`def show.sect(sect: Sect) -> String: Sect{nn, ps} = sect String.join(nn <> show.kvs(ps), " ")# every section of a document, showndef show.sects(ss: List<&2, Sect>) -> List<&2, String>: match ss: case []: [] case hd <> tl: show.sect(hd) <> show.sects(tl)