1 import Base 2 import ./json.bend as J 3 4 # YAML 5 # ==== 6 # 7 # The block YAML that kubeconfigs are written in, read into JSON, so a 8 # kubeconfig needs no kubectl to load. Covered: block mappings and 9 # sequences (a sequence may sit at its parent key's indentation, as 10 # kubectl writes it), "- key: value" items, plain, 'single' and "double" 11 # quoted scalars, null/~/true/false, comments, "---", and flow values that 12 # are JSON ({}, [], ["a", "b"]). Not covered: anchors, tags, block 13 # scalars (| >), multi-document streams, and non-JSON flow collections. 14 # 15 # A pushdown automaton over lines: open containers sit on a stack, each 16 # with the indentation it was opened at, and a line first closes every 17 # container deeper than itself. 18 19 type Line is Data: 20 Line{indent: U32, text: String} 21 22 # An open container: a mapping (members reversed, and the key whose 23 # value is still to come) or a sequence (items reversed, and, after a bare 24 # "- ", the anchor of the item whose block is still to come). 25 type YFrame is Data: 26 YMap{indent: U32, members: List<&2, J.KV>, pend: Maybe<&2, String>} 27 YSeq{indent: U32, items: List<&2, J.Json>, pend: Maybe<&2, String>} 28 YRoot{value: J.Json} 29 30 # mode: 0 between tokens, 1 in "..", 2 in '..', 3 in a plain scalar; 31 # buf: the plain scalar so far (reversed); out: JSON text (reversed). 32 type Flow is Data: 33 Flow{mode: U32, buf: String, out: String, esc: Bool} 34 35 # A block scalar being read: its key line's indent and text before the 36 # indicator, literal (|) or folded (>), chomping (0 clip, 1 strip -, 2 37 # keep +), the content's indent (0 until its first line), and its lines 38 # so far (reversed, the content indent removed). 39 type Blk is Data: 40 Blk{ki: U32, prefix: String, folded: Bool, chomp: U32, bi: U32, acc: List<&2, String>} 41 42 type BH is Data: 43 BH{prefix: String, folded: Bool, chomp: U32} 44 45 # The line fold: lines out (reversed), and the block scalar, if inside one. 46 type LS is Data: 47 LS{out: List<&2, Line>, blk: Maybe<&2, Blk>} 48 49 # A value with its anchor (&name) split off. 50 type AV is Data: 51 AV{anchor: String, rest: String} 52 53 type YR is Data: 54 YR{json: J.Json, anchors: Map<&2, J.Json>} 55 56 # Lines 57 # ----- 58 59 def Yaml.indent(s: String, +n: U32) -> U32: 60 match s: 61 case SCon{' ', t}: 62 Yaml.indent(t, (n + 1 : U32)) 63 case _: 64 n 65 66 def Yaml.quote.go(+q: U32, +k: U32, none: Bool, same: Bool) -> U32: 67 match none same: 68 case True{} _: 69 k 70 case False{} True{}: 71 0 72 case _ _: 73 q 74 75 # Entering or leaving a quote of kind k (1 single, 2 double). 76 def Yaml.quote(+q: U32, +k: U32) -> U32: 77 Yaml.quote.go(q, k, U32.is_eq(q, 0), U32.is_eq(q, k)) 78 79 # The quote state after char c. 80 def Yaml.nq(+c: Char, +q: U32) -> U32: 81 match c: 82 case '"': 83 Yaml.quote(q, 2) 84 case '\'': 85 Yaml.quote(q, 1) 86 case _: 87 q 88 89 # Whether a comment starts at the head of s: a '#' after a space, outside 90 # quotes. 91 def Yaml.cut_at(s: String, +q: U32, prev: Bool) -> Bool: 92 match s: 93 case SCon{'#', t}: 94 Bool.and(prev, U32.is_eq(q, 0)) 95 case _: 96 False{} 97 98 # Drops a comment, to the end of the line. 99 def Yaml.uncomment(s: String, +q: U32, cut: Bool) -> String: 100 match s cut: 101 case _ True{}: 102 SNil{} 103 case SNil{} False{}: 104 SNil{} 105 case SCon{+c, +t} False{}: 106 SCon{c, Yaml.uncomment(t, Yaml.nq(c, q), Yaml.cut_at(t, Yaml.nq(c, q), Char.is_space(c)))} 107 108 def Yaml.keep(+t: String) -> Bool: 109 Bool.and(Bool.not(String.is_empty(t)), Bool.not(String.eq(t, "---"))) 110 111 def Yaml.lines.put.go(+i: U32, text: String, rest: List<&2, Line>, keep: Bool) -> List<&2, Line>: 112 match keep: 113 case True{}: 114 Line{i, text} <> rest 115 case False{}: 116 rest 117 118 def Yaml.lines.put(+i: U32, +text: String, rest: List<&2, Line>) -> List<&2, Line>: 119 Yaml.lines.put.go(i, text, rest, Yaml.keep(text)) 120 121 # A block scalar's header ("key: |", "- >-", ...): the text before the 122 # indicator, its style and chomping. 123 def Yaml.head.go(r: String) -> Maybe<&2, BH>: 124 match r: 125 case SCon{'|', SCon{' ', t}}: 126 Some{BH{String.reverse(t) ++ " ", False{}, 0}} 127 case SCon{'>', SCon{' ', t}}: 128 Some{BH{String.reverse(t) ++ " ", True{}, 0}} 129 case SCon{'-', SCon{'|', SCon{' ', t}}}: 130 Some{BH{String.reverse(t) ++ " ", False{}, 1}} 131 case SCon{'-', SCon{'>', SCon{' ', t}}}: 132 Some{BH{String.reverse(t) ++ " ", True{}, 1}} 133 case SCon{'+', SCon{'|', SCon{' ', t}}}: 134 Some{BH{String.reverse(t) ++ " ", False{}, 2}} 135 case SCon{'+', SCon{'>', SCon{' ', t}}}: 136 Some{BH{String.reverse(t) ++ " ", True{}, 2}} 137 case _: 138 None{} 139 140 def Yaml.head(t: String) -> Maybe<&2, BH>: 141 Yaml.head.go(String.reverse(t)) 142 143 def Yaml.clean(+l: String) -> String: 144 String.trim(Yaml.uncomment(l, 0, Yaml.cut_at(l, 0, True{}))) 145 146 # Trailing blank lines (of a reversed list): how many, and the rest. 147 def Yaml.blanks(xs: List<&2, String>, +n: U32) -> U32: 148 match xs: 149 case Con{SNil{}, t}: 150 Yaml.blanks(t, (n + 1 : U32)) 151 case _: 152 n 153 154 def Yaml.unblank(xs: List<&2, String>) -> List<&2, String>: 155 match xs: 156 case Con{SNil{}, t}: 157 Yaml.unblank(t) 158 case x: 159 x 160 161 def Yaml.fold.sep(prev: Bool) -> String: 162 match prev: 163 case True{}: 164 " " 165 case False{}: 166 "" 167 168 # Folded lines: a line break between two text lines becomes a space; blank 169 # lines stay line breaks. 170 def Yaml.fold(xs: List<&2, String>, prev: Bool) -> String: 171 match xs: 172 case Nil{}: 173 "" 174 case Con{SNil{}, t}: 175 "\n" ++ Yaml.fold(t, False{}) 176 case Con{l, t}: 177 Yaml.fold.sep(prev) ++ l ++ Yaml.fold(t, True{}) 178 179 def Yaml.block.body(lines: List<&2, String>, folded: Bool) -> String: 180 match folded: 181 case True{}: 182 Yaml.fold(lines, False{}) 183 case False{}: 184 String.join(lines, "\n") 185 186 def Yaml.block.tail.go(+chomp: U32, +n: U32, strip: Bool, keep: Bool) -> String: 187 match strip keep: 188 case True{} _: 189 "" 190 case False{} True{}: 191 String.repeat("\n", U32.to_nat((n + 1 : U32))) 192 case _ _: 193 "\n" 194 195 def Yaml.block.tail(+chomp: U32, +n: U32, empty: Bool) -> String: 196 match empty: 197 case True{}: 198 "" 199 case False{}: 200 Yaml.block.tail.go(chomp, n, U32.is_eq(chomp, 1), U32.is_eq(chomp, 2)) 201 202 # A finished block scalar, as the one line "prefix ". 203 def Yaml.block.line(b: Blk) -> Line: 204 match b: 205 case Blk{ki, prefix, folded, +chomp, bi, +acc}: 206 Line{ki, prefix ++ J.Json.show(J.JStr{ 207 Yaml.block.body(List.reverse(&2, String, Yaml.unblank(acc)), folded) 208 ++ Yaml.block.tail(chomp, Yaml.blanks(acc, 0), List.is_empty(&2, String, Yaml.unblank(acc)))})} 209 210 def Yaml.normal.go(out: List<&2, Line>, +i: U32, +t: String, h: Maybe<&2, BH>) -> LS: 211 match h: 212 case Some{BH{p, f, c}}: 213 LS{out, Some{Blk{i, p, f, c, 0, Nil{}}}} 214 case None{}: 215 LS{Yaml.lines.put(i, t, out), None{}} 216 217 # An ordinary line: kept if meaningful, or the start of a block scalar. 218 def Yaml.normal(out: List<&2, Line>, +raw: String) -> LS: 219 Yaml.normal.go(out, Yaml.indent(raw, 0), Yaml.clean(raw), Yaml.head(Yaml.clean(raw))) 220 221 def Yaml.in_block.go(out: List<&2, Line>, b: Blk, +raw: String, blank: Bool, deeper: Bool, 222 first: Bool) -> LS: 223 match b blank deeper first: 224 case Blk{ki, p, f, c, bi, acc} True{} _ _: 225 LS{out, Some{Blk{ki, p, f, c, bi, SNil{} <> acc}}} 226 case Blk{ki, p, f, c, bi, acc} False{} True{} True{}: 227 LS{out, Some{Blk{ki, p, f, c, Yaml.indent(raw, 0), String.drop(raw, U32.to_nat(Yaml.indent(raw, 0))) <> acc}}} 228 case Blk{ki, p, f, c, +bi, acc} False{} True{} False{}: 229 LS{out, Some{Blk{ki, p, f, c, bi, String.drop(raw, U32.to_nat(bi)) <> acc}}} 230 case +x False{} False{} _: 231 Yaml.normal(Yaml.block.line(x) <> out, raw) 232 233 # A raw line inside a block scalar: content while deeper than the key (or 234 # blank), else the block ends and the line is read as ordinary. 235 def Yaml.in_block(out: List<&2, Line>, b: Blk, +raw: String) -> LS: 236 match b: 237 case Blk{+ki, p, f, c, +bi, acc}: 238 Yaml.in_block.go(out, Blk{ki, p, f, c, bi, acc}, raw, String.is_empty(String.trim(raw)), 239 U32.is_gt(Yaml.indent(raw, 0), ki), U32.is_eq(bi, 0)) 240 241 def Yaml.step_line(st: LS, +raw: String) -> LS: 242 match st: 243 case LS{out, None{}}: 244 Yaml.normal(out, raw) 245 case LS{out, Some{b}}: 246 Yaml.in_block(out, b, raw) 247 248 def Yaml.lines.fold(ls: List<&2, String>, st: LS) -> LS: 249 match ls: 250 case Nil{}: 251 st 252 case Con{l, t}: 253 Yaml.lines.fold(t, Yaml.step_line(st, l)) 254 255 def Yaml.lines.fin(st: LS) -> List<&2, Line>: 256 match st: 257 case LS{out, None{}}: 258 List.reverse(&2, Line, out) 259 case LS{out, Some{b}}: 260 List.reverse(&2, Line, Yaml.block.line(b) <> out) 261 262 # The meaningful lines: indentation and text, comments and blanks gone, 263 # block scalars (| and >) folded into one line each. 264 def Yaml.lines(s: String) -> List<&2, Line>: 265 Yaml.lines.fin(Yaml.lines.fold(String.lines(s), LS{Nil{}, None{}})) 266 267 # Scalars 268 # ------- 269 270 def Yaml.unsingle(s: String) -> String: 271 match s: 272 case SNil{}: 273 SNil{} 274 case SCon{'\'', SCon{'\'', t}}: 275 SCon{'\'', Yaml.unsingle(t)} 276 case SCon{c, t}: 277 SCon{c, Yaml.unsingle(t)} 278 279 def Yaml.strip_ends(+s: String) -> String: 280 String.take(String.drop(s, 1n), Nat.sub(String.length(s), 2n)) 281 282 def Yaml.json_or(r: Result<&2, &2, String, J.Json>, s: String) -> J.Json: 283 match r: 284 case Done{j}: 285 j 286 case Fail{e}: 287 J.JStr{s} 288 289 def Yaml.digits(s: String) -> Bool: 290 match s: 291 case SNil{}: 292 True{} 293 case SCon{+c, t}: 294 Bool.and(Char.is_digit(c), Yaml.digits(t)) 295 296 # Plain integers are numbers, as YAML 1.2's core schema reads them. 297 def Yaml.plain.num(x: String, int: Bool) -> J.Json: 298 match int: 299 case True{}: 300 J.JNum{x} 301 case False{}: 302 J.JStr{x} 303 304 # A plain scalar: null, ~, true, false, or a string. 305 def Yaml.plain(s: String) -> J.Json: 306 match s: 307 case "null": 308 J.JNull{} 309 case "~": 310 J.JNull{} 311 case "true": 312 J.JBool{True{}} 313 case "false": 314 J.JBool{False{}} 315 case +x: 316 Yaml.plain.num(x, Bool.and(Bool.not(String.is_empty(x)), Yaml.digits(x))) 317 318 def Flow.peek(s: String) -> Char: 319 match s: 320 case SCon{c, t}: 321 c 322 case SNil{}: 323 ' ' 324 325 # "key: rest" or "key:": the key (unquoted) and the rest, if this is a 326 # mapping entry. 327 type KR is Data: 328 KR{key: String, rest: String} 329 330 def Flow.punct(+c: Char) -> Bool: 331 match c: 332 case '{': 333 True{} 334 case '}': 335 True{} 336 case '[': 337 True{} 338 case ']': 339 True{} 340 case ',': 341 True{} 342 case ':': 343 True{} 344 case _: 345 False{} 346 347 def Flow.out0.go(+c: Char, out: String, punct: Bool, space: Bool) -> Flow: 348 match punct space: 349 case True{} _: 350 Flow{0, "", SCon{c, out}, False{}} 351 case False{} True{}: 352 Flow{0, "", out, False{}} 353 case False{} False{}: 354 Flow{3, SCon{c, SNil{}}, out, False{}} 355 356 def Flow.out0(+c: Char, out: String) -> Flow: 357 match c: 358 case '"': 359 Flow{1, "", SCon{'"', out}, False{}} 360 case '\'': 361 Flow{2, "", SCon{'"', out}, False{}} 362 case x: 363 Flow.out0.go(x, out, Flow.punct(x), Char.is_space(x)) 364 365 def Flow.dq(esc: Bool, +c: Char, out: String) -> Flow: 366 match esc c: 367 case True{} x: 368 Flow{1, "", SCon{x, out}, False{}} 369 case False{} '\\': 370 Flow{1, "", SCon{'\\', out}, True{}} 371 case False{} '"': 372 Flow{0, "", SCon{'"', out}, False{}} 373 case False{} x: 374 Flow{1, "", SCon{x, out}, False{}} 375 376 def Flow.sq.quote(out: String, doubled: Bool) -> Flow: 377 match doubled: 378 case True{}: 379 Flow{2, "", out, True{}} 380 case False{}: 381 Flow{0, "", SCon{'"', out}, False{}} 382 383 def Flow.sq(esc: Bool, +c: Char, +nxt: Char, out: String) -> Flow: 384 match esc c: 385 case True{} _: 386 Flow{2, "", SCon{'\'', out}, False{}} 387 case False{} '\'': 388 Flow.sq.quote(out, Char.is_eq(nxt, '\'')) 389 case False{} '"': 390 Flow{2, "", SCon{'"', SCon{'\\', out}}, False{}} 391 case False{} '\\': 392 Flow{2, "", SCon{'\\', SCon{'\\', out}}, False{}} 393 case False{} x: 394 Flow{2, "", SCon{x, out}, False{}} 395 396 def Flow.closer(+c: Char) -> Bool: 397 match c: 398 case '}': 399 True{} 400 case ']': 401 True{} 402 case ',': 403 True{} 404 case _: 405 False{} 406 407 # A plain scalar ends at a closer, or at ':' before a space or a closer. 408 def Flow.ends(+c: Char, +nxt: Char) -> Bool: 409 Bool.or(Flow.closer(c), Bool.and(Char.is_eq(c, ':'), Bool.or(Char.is_space(nxt), Flow.closer(nxt)))) 410 411 def Flow.emit(buf: String, out: String) -> String: 412 String.reverse(J.Json.show(Yaml.plain(String.trim(String.reverse(buf))))) ++ out 413 414 def Flow.plain.go(+c: Char, buf: String, out: String, ends: Bool) -> Flow: 415 match ends: 416 case True{}: 417 Flow{0, "", SCon{c, Flow.emit(buf, out)}, False{}} 418 case False{}: 419 Flow{3, SCon{c, buf}, out, False{}} 420 421 def Flow.plain(+c: Char, +nxt: Char, buf: String, out: String) -> Flow: 422 Flow.plain.go(c, buf, out, Flow.ends(c, nxt)) 423 424 def Flow.step.go(+m: U32, b: String, o: String, e: Bool, +c: Char, +nxt: Char, m0: Bool, m1: Bool, 425 m2: Bool) -> Flow: 426 match m0 m1 m2: 427 case True{} _ _: 428 Flow.out0(c, o) 429 case False{} True{} _: 430 Flow.dq(e, c, o) 431 case False{} False{} True{}: 432 Flow.sq(e, c, nxt, o) 433 case _ _ _: 434 Flow.plain(c, nxt, b, o) 435 436 def Flow.step(st: Flow, +c: Char, +nxt: Char) -> Flow: 437 match st: 438 case Flow{+m, b, o, e}: 439 Flow.step.go(m, b, o, e, c, nxt, U32.is_eq(m, 0), U32.is_eq(m, 1), U32.is_eq(m, 2)) 440 441 def Flow.go(s: String, st: Flow) -> Flow: 442 match s: 443 case SNil{}: 444 st 445 case SCon{+c, +t}: 446 Flow.go(t, Flow.step(st, c, Flow.peek(t))) 447 448 def Flow.fin.go(b: String, o: String, plain: Bool) -> String: 449 match plain: 450 case True{}: 451 Flow.emit(b, o) 452 case False{}: 453 o 454 455 def Flow.fin(st: Flow) -> String: 456 match st: 457 case Flow{+m, b, o, e}: 458 String.reverse(Flow.fin.go(b, o, U32.is_eq(m, 3))) 459 460 # A flow value's JSON text. 461 def Yaml.flow(s: String) -> String: 462 Flow.fin(Flow.go(s, Flow{0, "", "", False{}})) 463 464 # Anchors, aliases, tags 465 # ---------------------- 466 # 467 # The line parser leaves markers: an anchored value is the object 468 # {"\u0000&name": value}, an alias the string "\u0000*name", and a key 469 # whose anchored value is still to come is "\u0001name\u0001key". After 470 # parsing, Yaml.resolve walks the document in order, recording anchors and 471 # replacing aliases (an alias names an earlier anchor), and applies merge 472 # keys (<<: *base), whose members fill in keys the mapping lacks. 473 474 def Yaml.split_word.go(s: String, acc: String) -> AV: 475 match s: 476 case SNil{}: 477 AV{String.reverse(acc), ""} 478 case SCon{' ', t}: 479 AV{String.reverse(acc), String.trim(t)} 480 case SCon{c, t}: 481 Yaml.split_word.go(t, SCon{c, acc}) 482 483 # The first word and the rest. 484 def Yaml.split_word(s: String) -> AV: 485 Yaml.split_word.go(s, "") 486 487 def Yaml.scalar.untagged(+s: String) -> J.Json: 488 match s: 489 case SCon{'"', t}: 490 Yaml.json_or(J.Json.parse(s), s) 491 case SCon{'\'', t}: 492 J.JStr{Yaml.unsingle(Yaml.strip_ends(s))} 493 case x: 494 Yaml.plain(x) 495 496 # A tagged scalar: !!str keeps the text a string; other tags are dropped. 497 def Yaml.tagged(av: AV) -> J.Json: 498 match av: 499 case AV{"!str", r}: 500 J.JStr{r} 501 case AV{t, +r}: 502 Yaml.scalar.untagged(r) 503 504 # A scalar or flow value; an alias (*name) or a tag (!!str x) first. 505 def Yaml.scalar(+s: String) -> J.Json: 506 match s: 507 case SCon{'*', t}: 508 J.JStr{SCon{Chr{0}, SCon{'*', t}}} 509 case SCon{'!', t}: 510 Yaml.tagged(Yaml.split_word(t)) 511 case SCon{'"', t}: 512 Yaml.json_or(J.Json.parse(s), s) 513 case SCon{'\'', t}: 514 J.JStr{Yaml.unsingle(Yaml.strip_ends(s))} 515 case SCon{'[', t}: 516 Yaml.json_or(J.Json.parse(Yaml.flow(s)), s) 517 case SCon{'{', t}: 518 Yaml.json_or(J.Json.parse(Yaml.flow(s)), s) 519 case x: 520 Yaml.plain(x) 521 522 def Yaml.key.go(s: String, acc: String) -> Maybe<&2, KR>: 523 match s: 524 case SNil{}: 525 None{} 526 case SCon{':', SNil{}}: 527 Some{KR{String.reverse(acc), ""}} 528 case SCon{':', SCon{' ', t}}: 529 Some{KR{String.reverse(acc), String.trim(t)}} 530 case SCon{c, t}: 531 Yaml.key.go(t, SCon{c, acc}) 532 533 def Yaml.unquote_key(+k: String) -> String: 534 match k: 535 case SCon{'"', t}: 536 Yaml.strip_ends(k) 537 case SCon{'\'', t}: 538 Yaml.strip_ends(k) 539 case x: 540 x 541 542 def Yaml.key.fin(m: Maybe<&2, KR>) -> Maybe<&2, KR>: 543 match m: 544 case Some{KR{k, r}}: 545 Some{KR{Yaml.unquote_key(String.trim(k)), r}} 546 case None{}: 547 None{} 548 549 def Yaml.key(+s: String) -> Maybe<&2, KR>: 550 match s: 551 case SCon{'"', t}: 552 Yaml.key.fin(Yaml.key.go(s, "")) 553 case SCon{'[', t}: 554 None{} 555 case SCon{'{', t}: 556 None{} 557 case x: 558 Yaml.key.fin(Yaml.key.go(x, "")) 559 560 # The stack 561 # --------- 562 563 def Yaml.rev_kv(xs: List<&2, J.KV>, acc: J.Json) -> J.Json: 564 match xs: 565 case Nil{}: 566 acc 567 case Con{J.KV{k, v}, t}: 568 Yaml.rev_kv(t, J.JMem{k, v, acc}) 569 570 def Yaml.rev_items(xs: List<&2, J.Json>, acc: J.Json) -> J.Json: 571 match xs: 572 case Nil{}: 573 acc 574 case Con{h, t}: 575 Yaml.rev_items(t, J.JCons{h, acc}) 576 577 def Yaml.pend_null(p: Maybe<&2, String>, ms: List<&2, J.KV>) -> List<&2, J.KV>: 578 match p: 579 case Some{k}: 580 J.KV{k, J.JNull{}} <> ms 581 case None{}: 582 ms 583 584 def Yaml.wrap.go(a: String, v: J.Json, none: Bool) -> J.Json: 585 match none: 586 case True{}: 587 v 588 case False{}: 589 J.JObj{J.JMem{SCon{Chr{0}, SCon{'&', a}}, v, J.JNil{}}} 590 591 def Yaml.wrap(+a: String, v: J.Json) -> J.Json: 592 Yaml.wrap.go(a, v, String.is_empty(a)) 593 594 # A finished container's value. 595 def Yaml.value(f: YFrame) -> J.Json: 596 match f: 597 case YMap{i, ms, p}: 598 J.JObj{Yaml.rev_kv(Yaml.pend_null(p, ms), J.JNil{})} 599 case YSeq{i, xs, None{}}: 600 J.JArr{Yaml.rev_items(xs, J.JNil{})} 601 case YSeq{i, xs, Some{a}}: 602 J.JArr{Yaml.rev_items(Yaml.wrap(a, J.JNull{}) <> xs, J.JNil{})} 603 case YRoot{v}: 604 v 605 606 # Whether s starts with the control char of code n. 607 def Yaml.ctl(s: String, +n: U32) -> Bool: 608 match s: 609 case SCon{Chr{+c}, t}: 610 U32.is_eq(c, n) 611 case _: 612 False{} 613 614 def Yaml.member.split(parts: List<&2, String>, v: J.Json) -> J.KV: 615 match parts: 616 case Con{a, Con{k, rest}}: 617 J.KV{String.join(k <> rest, SCon{Chr{1}, SNil{}}), Yaml.wrap(a, v)} 618 case _: 619 J.KV{"", v} 620 621 def Yaml.member.go(k: String, v: J.Json, marked: Bool) -> J.KV: 622 match marked: 623 case True{}: 624 Yaml.member.split(String.split(String.drop(k, 1n), Chr{1}), v) 625 case False{}: 626 J.KV{k, v} 627 628 # A pending key's member, its anchor (if marked) wrapped around v. 629 def Yaml.member(+k: String, v: J.Json) -> J.KV: 630 Yaml.member.go(k, v, Yaml.ctl(k, 1)) 631 632 # Hands v to the container on top: a mapping's pending key, a sequence's 633 # next item, or the document itself. 634 def Yaml.give(v: J.Json, st: List<&2, YFrame>) -> List<&2, YFrame>: 635 match st: 636 case Con{YMap{i, ms, Some{k}}, t}: 637 YMap{i, Yaml.member(k, v) <> ms, None{}} <> t 638 case Con{YSeq{i, xs, None{}}, t}: 639 YSeq{i, v <> xs, None{}} <> t 640 case Con{YSeq{i, xs, Some{a}}, t}: 641 YSeq{i, Yaml.wrap(a, v) <> xs, None{}} <> t 642 case Con{YMap{i, ms, None{}}, t}: 643 YMap{i, ms, None{}} <> t 644 case Con{YRoot{x}, t}: 645 YRoot{v} <> t 646 case Nil{}: 647 [YRoot{v}] 648 649 def Yaml.top_indent(st: List<&2, YFrame>) -> U32: 650 match st: 651 case Con{YMap{i, ms, p}, t}: 652 i 653 case Con{YSeq{i, xs, p}, t}: 654 i 655 case _: 656 0 657 658 # Whether the top container ends before a line at indent i: it is deeper, 659 # or it is a sequence at i and the line is no item of it. 660 def Yaml.ends(+st: List<&2, YFrame>, +i: U32, dash: Bool) -> Bool: 661 match st: 662 case Con{YMap{+j, ms, p}, t}: 663 U32.is_gt(j, i) 664 case Con{YSeq{+j, xs, p}, t}: 665 Bool.or(U32.is_gt(j, i), Bool.and(U32.is_eq(j, i), Bool.not(dash))) 666 case _: 667 False{} 668 669 def Yaml.pop(st: List<&2, YFrame>) -> List<&2, YFrame>: 670 match st: 671 case Con{f, t}: 672 Yaml.give(Yaml.value(f), t) 673 case Nil{}: 674 Nil{} 675 676 def Yaml.close(fuel: Nat, +st: List<&2, YFrame>, +i: U32, +dash: Bool, ends: Bool) -> List<&2, YFrame>: 677 match fuel ends: 678 case 1n+f True{}: 679 Yaml.close(f, Yaml.pop(st), i, dash, Yaml.ends(Yaml.pop(st), i, dash)) 680 case _ _: 681 st 682 683 def Yaml.map_at(+st: List<&2, YFrame>, +i: U32) -> Bool: 684 match st: 685 case Con{YMap{+j, ms, p}, t}: 686 U32.is_eq(j, i) 687 case _: 688 False{} 689 690 def Yaml.mark.go(a: String, k: String, none: Bool) -> String: 691 match none: 692 case True{}: 693 k 694 case False{}: 695 SCon{Chr{1}, a ++ SCon{Chr{1}, k}} 696 697 def Yaml.mark(+a: String, k: String) -> String: 698 Yaml.mark.go(a, k, String.is_empty(a)) 699 700 def Yaml.entry.put.go(st: List<&2, YFrame>, k: String, +a: String, +r: String, empty: Bool) -> 701 List<&2, YFrame>: 702 match st empty: 703 case Con{YMap{i, ms, p}, t} True{}: 704 YMap{i, ms, Some{Yaml.mark(a, k)}} <> t 705 case Con{YMap{i, ms, p}, t} False{}: 706 YMap{i, J.KV{k, Yaml.wrap(a, Yaml.scalar(r))} <> ms, None{}} <> t 707 case s _: 708 s 709 710 def Yaml.entry.put.av(st: List<&2, YFrame>, k: String, av: AV) -> List<&2, YFrame>: 711 match av: 712 case AV{+a, +r}: 713 Yaml.entry.put.go(st, k, a, r, String.is_empty(r)) 714 715 def Yaml.split_anchor(+s: String) -> AV: 716 match s: 717 case SCon{'&', t}: 718 Yaml.split_word(t) 719 case x: 720 AV{"", x} 721 722 def Yaml.entry.put(st: List<&2, YFrame>, k: String, +r: String, empty: Bool) -> List<&2, YFrame>: 723 Yaml.entry.put.av(st, k, Yaml.split_anchor(r)) 724 725 # Flow collections 726 # ---------------- 727 # 728 # A flow value ({a: b, c: [d, 'e']}) becomes JSON text: quoted scalars are 729 # re-quoted as JSON strings, plain ones read as scalars, punctuation kept. 730 731 def Yaml.entry.go(st: List<&2, YFrame>, +i: U32, kr: KR, here: Bool) -> List<&2, YFrame>: 732 match kr here: 733 case KR{k, +r} True{}: 734 Yaml.entry.put(st, k, r, String.is_empty(r)) 735 case KR{k, +r} False{}: 736 Yaml.entry.put(YMap{i, Nil{}, None{}} <> st, k, r, String.is_empty(r)) 737 738 # A mapping entry at indent i: into the open mapping at i, or a new one. 739 def Yaml.entry(+st: List<&2, YFrame>, +i: U32, kr: KR) -> List<&2, YFrame>: 740 Yaml.entry.go(st, i, kr, Yaml.map_at(st, i)) 741 742 def Yaml.seq_at(+st: List<&2, YFrame>, +i: U32) -> Bool: 743 match st: 744 case Con{YSeq{+j, xs, p}, t}: 745 U32.is_eq(j, i) 746 case _: 747 False{} 748 749 # A new item: an item still waiting for its block got none, so it is null. 750 def Yaml.item.seq.go(st: List<&2, YFrame>, +i: U32, here: Bool) -> List<&2, YFrame>: 751 match st here: 752 case Con{YSeq{j, xs, Some{a}}, t} True{}: 753 YSeq{j, Yaml.wrap(a, J.JNull{}) <> xs, None{}} <> t 754 case x True{}: 755 x 756 case x False{}: 757 YSeq{i, Nil{}, None{}} <> x 758 759 def Yaml.item.seq(+st: List<&2, YFrame>, +i: U32) -> List<&2, YFrame>: 760 Yaml.item.seq.go(st, i, Yaml.seq_at(st, i)) 761 762 def Yaml.item.value.go(st: List<&2, YFrame>, +a: String, +r: String, empty: Bool) -> List<&2, YFrame>: 763 match st empty: 764 case Con{YSeq{i, xs, p}, t} True{}: 765 YSeq{i, xs, Some{a}} <> t 766 case x _: 767 Yaml.give(Yaml.wrap(a, Yaml.scalar(r)), x) 768 769 # "- &a value", "- *a", "- " (a block follows), or "- &a" (an anchored 770 # block follows). 771 def Yaml.item.value(st: List<&2, YFrame>, av: AV) -> List<&2, YFrame>: 772 match av: 773 case AV{+a, +r}: 774 Yaml.item.value.go(st, a, r, String.is_empty(r)) 775 776 # The item is a mapping when it reads "key: ..." (its keys then sit at 777 # i + 2), else a scalar. 778 def Yaml.item.go(st: List<&2, YFrame>, +i: U32, rest: String, kr: Maybe<&2, KR>) -> List<&2, YFrame>: 779 match kr: 780 case Some{x}: 781 Yaml.entry.go(st, (i + 2 : U32), x, False{}) 782 case None{}: 783 Yaml.item.value(st, Yaml.split_anchor(rest)) 784 785 # A "- item" line at indent i: into the open sequence at i, or a new one. 786 def Yaml.item(+st: List<&2, YFrame>, +i: U32, +rest: String) -> List<&2, YFrame>: 787 Yaml.item.go(Yaml.item.seq(st, i), i, rest, Yaml.key(rest)) 788 789 def Yaml.is_dash(t: String) -> Bool: 790 match t: 791 case SCon{'-', SCon{' ', r}}: 792 True{} 793 case "-": 794 True{} 795 case _: 796 False{} 797 798 def Yaml.after_dash(t: String) -> String: 799 String.trim(String.drop(t, 1n)) 800 801 def Yaml.step.entry(+st: List<&2, YFrame>, +i: U32, +t: String, kr: Maybe<&2, KR>) -> List<&2, YFrame>: 802 match kr: 803 case Some{x}: 804 Yaml.entry(st, i, x) 805 case None{}: 806 Yaml.give(Yaml.scalar(t), st) 807 808 def Yaml.step.go(+st: List<&2, YFrame>, +i: U32, +t: String, dash: Bool) -> List<&2, YFrame>: 809 match dash: 810 case True{}: 811 Yaml.item(st, i, Yaml.after_dash(t)) 812 case False{}: 813 Yaml.step.entry(st, i, t, Yaml.key(t)) 814 815 # One line. 816 def Yaml.step(+st: List<&2, YFrame>, l: Line) -> List<&2, YFrame>: 817 match l: 818 case Line{+i, +t}: 819 Yaml.step.go(Yaml.close(64n, st, i, Yaml.is_dash(t), Yaml.ends(st, i, Yaml.is_dash(t))), i, t, 820 Yaml.is_dash(t)) 821 822 def Yaml.feed(ls: List<&2, Line>, +st: List<&2, YFrame>) -> List<&2, YFrame>: 823 match ls: 824 case Nil{}: 825 st 826 case Con{l, t}: 827 Yaml.feed(t, Yaml.step(st, l)) 828 829 def Yaml.result(st: List<&2, YFrame>) -> J.Json: 830 match st: 831 case Con{YRoot{v}, t}: 832 v 833 case Con{f, t}: 834 Yaml.value(f) 835 case Nil{}: 836 J.JNull{} 837 838 def Yaml.is_open(st: List<&2, YFrame>) -> Bool: 839 match st: 840 case Con{YRoot{v}, t}: 841 False{} 842 case Con{f, t}: 843 True{} 844 case Nil{}: 845 False{} 846 847 def Yaml.close_all.go(fuel: Nat, +st: List<&2, YFrame>, open: Bool) -> List<&2, YFrame>: 848 match fuel open: 849 case 1n+f True{}: 850 Yaml.close_all.go(f, Yaml.pop(st), Yaml.is_open(Yaml.pop(st))) 851 case _ _: 852 st 853 854 # Closes every open container, into the document. 855 def Yaml.close_all(fuel: Nat, +st: List<&2, YFrame>) -> List<&2, YFrame>: 856 Yaml.close_all.go(fuel, st, Yaml.is_open(st)) 857 858 def YR.json(r: YR) -> J.Json: 859 match r: 860 case YR{j, a}: 861 j 862 863 # Resolution 864 # ---------- 865 866 867 def YR.anchors(r: YR) -> Map<&2, J.Json>: 868 match r: 869 case YR{j, a}: 870 a 871 872 def Yaml.marker.go(n: String, hit: Bool) -> Maybe<&2, String>: 873 match hit: 874 case True{}: 875 Some{n} 876 case False{}: 877 None{} 878 879 def Yaml.marker(s: String, +kind: Char) -> Maybe<&2, String>: 880 match s: 881 case SCon{Chr{+c}, SCon{+k, n}}: 882 Yaml.marker.go(n, Bool.and(U32.is_eq(c, 0), Char.is_eq(k, kind))) 883 case _: 884 None{} 885 886 def Yaml.anchor_name(k: String) -> Maybe<&2, String>: 887 Yaml.marker(k, '&') 888 889 def Yaml.res.mem(k: String, h: YR, t: YR) -> YR: 890 match h t: 891 case YR{v, a} YR{rest, a2}: 892 YR{J.JMem{k, v, rest}, a2} 893 894 def Yaml.res.cons(h: YR, t: YR) -> YR: 895 match h t: 896 case YR{v, a} YR{rest, a2}: 897 YR{J.JCons{v, rest}, a2} 898 899 def Yaml.merge.take(+ms: J.Json) -> J.Json: 900 J.Json.lookup(ms, "<<") 901 902 def Yaml.merge.missing.put(k: String, v: J.Json, rest: J.Json, h: Bool, has: Bool) -> J.Json: 903 match has: 904 case True{}: 905 rest 906 case False{}: 907 J.JMem{k, v, rest} 908 909 def Yaml.has(ms: J.Json, +k: String) -> Bool: 910 Bool.not(J.Json.is_null(J.Json.lookup(ms, k))) 911 912 # Members whose keys the chain ms lacks, from the merged object's chain. 913 def Yaml.merge.missing(src: J.Json, +ms: J.Json) -> J.Json: 914 match src: 915 case J.JMem{+k, v, t}: 916 Yaml.merge.missing.put(k, v, Yaml.merge.missing(t, ms), J.Json.head_is(ms, k), Yaml.has(ms, k)) 917 case _: 918 J.JNil{} 919 920 def Yaml.append_mem(ms: J.Json, extra: J.Json) -> J.Json: 921 match ms: 922 case J.JMem{k, v, t}: 923 J.JMem{k, v, Yaml.append_mem(t, extra)} 924 case _: 925 extra 926 927 def Yaml.merge.go(src: J.Json, +rest: J.Json) -> J.Json: 928 match src: 929 case J.JObj{m}: 930 Yaml.append_mem(rest, Yaml.merge.missing(m, rest)) 931 case _: 932 rest 933 934 # Applies a "<<" member: its object's members join where keys are absent. 935 def Yaml.merge(+ms: J.Json) -> J.Json: 936 Yaml.merge.go(Yaml.merge.take(ms), J.Json.del_mem(ms, "<<")) 937 938 def Yaml.res.obj(r: YR) -> YR: 939 match r: 940 case YR{ms, a}: 941 YR{J.JObj{Yaml.merge(ms)}, a} 942 943 def Yaml.res.arr(r: YR) -> YR: 944 match r: 945 case YR{xs, a}: 946 YR{J.JArr{xs}, a} 947 948 def Yaml.is_alias(s: String) -> Maybe<&2, String>: 949 Yaml.marker(s, '*') 950 951 def Yaml.lookup.fin(r: Map<&2, J.Json> & J.Json) -> J.Json: 952 match r: 953 case (m, v): 954 v 955 956 def Yaml.alias(+name: String, +anchors: Map<&2, J.Json>) -> J.Json: 957 Yaml.lookup.fin(Map.get(J.Json, J.JNull{}, anchors, name)) 958 959 def Yaml.res.str(s: String, +a: Map<&2, J.Json>, alias: Maybe<&2, String>) -> YR: 960 match alias: 961 case Some{n}: 962 YR{Yaml.alias(n, a), a} 963 case None{}: 964 YR{J.JStr{s}, a} 965 966 def Yaml.res.anchor(+n: String, r: YR) -> YR: 967 match r: 968 case YR{+v, a}: 969 YR{v, Map.set(&2, J.Json, a, n, v)} 970 971 # A one-member object: an anchor marker, or a plain object. 972 def Yaml.res.single(+k: String, n: Maybe<&2, String>, +rv: YR, tail: J.Json, a: Map<&2, J.Json>) -> YR: 973 match n: 974 case Some{name}: 975 Yaml.res.anchor(name, rv) 976 case None{}: 977 Yaml.res.obj(Yaml.res.mem(k, rv, YR{tail, YR.anchors(rv)})) 978 979 # The document with anchors recorded, aliases replaced and merges applied. 980 def Yaml.res(j: J.Json, +a: Map<&2, J.Json>) -> YR: 981 match j: 982 case J.JStr{+s}: 983 Yaml.res.str(s, a, Yaml.is_alias(s)) 984 case J.JObj{J.JMem{+k, v, J.JNil{}}}: 985 Yaml.res.single(k, Yaml.anchor_name(k), Yaml.res(v, a), J.JNil{}, a) 986 case J.JObj{ms}: 987 Yaml.res.obj(Yaml.res(ms, a)) 988 case J.JArr{xs}: 989 Yaml.res.arr(Yaml.res(xs, a)) 990 case J.JCons{h, t}: 991 +rh = Yaml.res(h, a) 992 Yaml.res.cons(rh, Yaml.res(t, YR.anchors(rh))) 993 case J.JMem{k, v, t}: 994 +rv = Yaml.res(v, a) 995 Yaml.res.mem(k, rv, Yaml.res(t, YR.anchors(rv))) 996 case x: 997 YR{x, a} 998 999 def Yaml.resolve(j: J.Json) -> J.Json: 1000 YR.json(Yaml.res(j, Map.new(&2, J.Json))) 1001 1002 # The (first) document as JSON, anchors and aliases resolved. 1003 def Yaml.parse(s: String) -> J.Json: 1004 Yaml.resolve(Yaml.result(Yaml.close_all(64n, Yaml.feed(Yaml.lines(s), Nil{})))) 1005 1006 # Documents 1007 # --------- 1008 1009 def Yaml.is_sep(+l: String) -> Bool: 1010 Bool.or(String.starts_with(l, "---"), String.eq(String.trim_end(l), "...")) 1011 1012 def Yaml.docs.cur(+l: String, cur: List<&2, String>, sep: Bool) -> List<&2, String>: 1013 match sep: 1014 case True{}: 1015 Nil{} 1016 case False{}: 1017 l <> cur 1018 1019 def Yaml.docs.done(+cur: List<&2, String>, done: List<&2, String>, sep: Bool) -> List<&2, String>: 1020 match sep: 1021 case True{}: 1022 String.join(List.reverse(&2, String, cur), "\n") <> done 1023 case False{}: 1024 done 1025 1026 # Splits a stream at "---" and "..." lines; documents come reversed. 1027 def Yaml.docs.go(ls: List<&2, String>, +cur: List<&2, String>, done: List<&2, String>) -> List<&2, String>: 1028 match ls: 1029 case Nil{}: 1030 String.join(List.reverse(&2, String, cur), "\n") <> done 1031 case Con{+l, t}: 1032 Yaml.docs.go(t, Yaml.docs.cur(l, cur, Yaml.is_sep(l)), Yaml.docs.done(cur, done, Yaml.is_sep(l))) 1033 1034 def Yaml.docs.keep(d: String, acc: List<&2, J.Json>, empty: Bool) -> List<&2, J.Json>: 1035 match empty: 1036 case True{}: 1037 acc 1038 case False{}: 1039 Yaml.parse(d) <> acc 1040 1041 def Yaml.docs.parse(ds: List<&2, String>, acc: List<&2, J.Json>) -> List<&2, J.Json>: 1042 match ds: 1043 case Nil{}: 1044 acc 1045 case Con{+d, t}: 1046 Yaml.docs.parse(t, Yaml.docs.keep(d, acc, List.is_empty(&2, Line, Yaml.lines(d)))) 1047 1048 # Every document of a stream (kubectl's multi-document files), in order; 1049 # empty documents are skipped. 1050 def Yaml.parse_all(s: String) -> List<&2, J.Json>: 1051 Yaml.docs.parse(Yaml.docs.go(String.lines(s), Nil{}, Nil{}), Nil{})