src/yaml.bend
bend-kube-runtime@0.1.1.0 · 1,051 lines · 0 definitions
1import Base
2import bend-kube-runtime@0.1.1.0/src/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
19type 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).
25type 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).
32type 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).
39type Blk is Data:
40 Blk{ki: U32, prefix: String, folded: Bool, chomp: U32, bi: U32, acc: List<&2, String>}
41
42type 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.
46type LS is Data:
47 LS{out: List<&2, Line>, blk: Maybe<&2, Blk>}
48
49# A value with its anchor (&name) split off.
50type AV is Data:
51 AV{anchor: String, rest: String}
52
53type YR is Data:
54 YR{json: J.Json, anchors: Map<&2, J.Json>}
55
56# Lines
57# -----
58
59def 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
66def 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).
76def 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.
80def 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.
91def 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.
99def 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
108def Yaml.keep(+t: String) -> Bool:
109 Bool.and(Bool.not(String.is_empty(t)), Bool.not(String.eq(t, "---")))
110
111def 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
118def 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.
123def 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
140def Yaml.head(t: String) -> Maybe<&2, BH>:
141 Yaml.head.go(String.reverse(t))
142
143def 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.
147def 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
154def 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
161def 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.
170def 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
179def 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
186def 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
195def 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 <JSON string>".
203def 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
210def 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.
218def 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
221def 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.
235def 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
241def 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
248def 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
255def 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.
264def 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
270def 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
279def Yaml.strip_ends(+s: String) -> String:
280 String.take(String.drop(s, 1n), Nat.sub(String.length(s), 2n))
281
282def 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
289def 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.
297def 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.
305def 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
318def 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.
327type KR is Data:
328 KR{key: String, rest: String}
329
330def 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
347def 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
356def 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
365def 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
376def 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
383def 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
396def 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.
408def 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
411def Flow.emit(buf: String, out: String) -> String:
412 String.reverse(J.Json.show(Yaml.plain(String.trim(String.reverse(buf))))) ++ out
413
414def 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
421def Flow.plain(+c: Char, +nxt: Char, buf: String, out: String) -> Flow:
422 Flow.plain.go(c, buf, out, Flow.ends(c, nxt))
423
424def 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
436def 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
441def 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
448def 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
455def 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.
461def 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
474def 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.
484def Yaml.split_word(s: String) -> AV:
485 Yaml.split_word.go(s, "")
486
487def 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.
497def 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.
505def 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
522def 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
533def 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
542def 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
549def 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
563def 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
570def 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
577def 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
584def 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
591def 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.
595def 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.
607def 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
614def 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
621def 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.
629def 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.
634def 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
649def 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.
660def 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
669def 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
676def 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
683def 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
690def 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
697def Yaml.mark(+a: String, k: String) -> String:
698 Yaml.mark.go(a, k, String.is_empty(a))
699
700def 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
710def 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
715def 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
722def 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
731def 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.
739def 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
742def 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.
750def 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
759def 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
762def 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).
771def 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.
778def 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.
786def 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
789def 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
798def Yaml.after_dash(t: String) -> String:
799 String.trim(String.drop(t, 1n))
800
801def 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
808def 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.
816def 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
822def 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
829def 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
838def 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
847def 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.
855def Yaml.close_all(fuel: Nat, +st: List<&2, YFrame>) -> List<&2, YFrame>:
856 Yaml.close_all.go(fuel, st, Yaml.is_open(st))
857
858def YR.json(r: YR) -> J.Json:
859 match r:
860 case YR{j, a}:
861 j
862
863# Resolution
864# ----------
865
866
867def YR.anchors(r: YR) -> Map<&2, J.Json>:
868 match r:
869 case YR{j, a}:
870 a
871
872def 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
879def 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
886def Yaml.anchor_name(k: String) -> Maybe<&2, String>:
887 Yaml.marker(k, '&')
888
889def 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
894def 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
899def Yaml.merge.take(+ms: J.Json) -> J.Json:
900 J.Json.lookup(ms, "<<")
901
902def 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
909def 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.
913def 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
920def 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
927def 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.
935def Yaml.merge(+ms: J.Json) -> J.Json:
936 Yaml.merge.go(Yaml.merge.take(ms), J.Json.del_mem(ms, "<<"))
937
938def Yaml.res.obj(r: YR) -> YR:
939 match r:
940 case YR{ms, a}:
941 YR{J.JObj{Yaml.merge(ms)}, a}
942
943def Yaml.res.arr(r: YR) -> YR:
944 match r:
945 case YR{xs, a}:
946 YR{J.JArr{xs}, a}
947
948def Yaml.is_alias(s: String) -> Maybe<&2, String>:
949 Yaml.marker(s, '*')
950
951def Yaml.lookup.fin(r: Map<&2, J.Json> & J.Json) -> J.Json:
952 match r:
953 case (m, v):
954 v
955
956def Yaml.alias(+name: String, +anchors: Map<&2, J.Json>) -> J.Json:
957 Yaml.lookup.fin(Map.get(J.Json, J.JNull{}, anchors, name))
958
959def 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
966def 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.
972def 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.
980def 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
999def 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.
1003def 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
1009def Yaml.is_sep(+l: String) -> Bool:
1010 Bool.or(String.starts_with(l, "---"), String.eq(String.trim_end(l), "..."))
1011
1012def 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
1019def 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.
1027def 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
1034def 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
1041def 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.
1050def Yaml.parse_all(s: String) -> List<&2, J.Json>:
1051 Yaml.docs.parse(Yaml.docs.go(String.lines(s), Nil{}, Nil{}), Nil{})