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. 14 import Base 15 import 0x8fb95168b7719a8faec16af7ee47b246/main.bend as E 16 17 # a key and the string written for it 18 type 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 22 type 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 28 type 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 33 def 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. 39 def 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 47 def 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 56 type 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. 62 def 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 68 type Split is Data: 69 Split{quoted: Bool, cur: List<&2, Char>, out: List<&2, String>} 70 71 # the segment gathered so far, in order 72 def 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 76 def 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 84 def 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 95 def 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 99 def 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 104 def 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. 113 def 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 118 def 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. 123 def 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. 130 def 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. 140 def 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 148 def 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 152 def 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 159 def 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 164 def 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. 173 def render.sect(sect: Sect) -> String: 174 Sect{nn, ps} = sect 175 "[" ++ nn ++ "]\n" ++ render.pairs(ps) 176 177 # every table of a document 178 def 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 187 def 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 191 def 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 198 def 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. 208 def 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 222 def 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 232 def 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 240 def 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 248 def 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 256 def 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) 270 def 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 277 def 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 281 def 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. 287 def 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 298 def 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 303 def 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` 310 def show.kv(kv: Kv) -> String: 311 Kv{kk, vv} = kv 312 kk ++ "=" ++ vv 313 314 # every pair of a section, shown 315 def 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 ..` 323 def 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 328 def 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)