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 0xc5d9d3e28b79ea98be22eb42c7e37802/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 chars gathered so far, last read first, spelled in the order they arrived 72 def 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 80 def 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 84 def 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 92 def 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 103 def 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 107 def 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 112 def 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. 121 def 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 126 def 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. 131 def 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. 138 def 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. 148 def 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 156 def 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 160 def 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 167 def 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 172 def 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. 181 def render.sect(sect: Sect) -> String: 182 Sect{nn, ps} = sect 183 "[" ++ nn ++ "]\n" ++ render.pairs(ps) 184 185 # every table of a document 186 def 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 195 def 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 199 def 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 206 def 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. 216 def 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 230 def 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 240 def 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 248 def 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 256 def 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 264 def 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) 278 def 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 285 def 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 289 def 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. 295 def 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 306 def 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 311 def 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` 318 def show.kv(kv: Kv) -> String: 319 Kv{kk, vv} = kv 320 kk ++ "=" ++ vv 321 322 # every pair of a section, shown 323 def 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 ..` 331 def 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 336 def 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)