src/decode.bend

bend-toon-codec@0.1.0.0 · 174 lines · 27 definitions

1import Base
2import bend-toon-codec@0.1.0.0/src/json.bend as J
3import bend-toon-codec@0.1.0.0/src/split.bend as S
4import bend-toon-codec@0.1.0.0/src/prim.bend as P
5import bend-toon-codec@0.1.0.0/src/hdr.bend as H
6import bend-toon-codec@0.1.0.0/src/entry.bend as E
7import bend-toon-codec@0.1.0.0/src/dline.bend as D
8import bend-toon-codec@0.1.0.0/src/frame.bend as F
9import bend-toon-codec@0.1.0.0/src/step.bend as T
10import bend-toon-codec@0.1.0.0/src/expand.bend as X
11import bend-toon-codec@0.1.0.0/src/head.bend as Hd
12
13# Decoder (spec sections 4, 5, 12 to 14): lines to a Json value.
14
15# Decoder options: spaces per indent level, strict mode, and path expansion
16# (section 13.4: unquoted dotted keys become nested objects).
17type DOpts is Data:
18  DOpts{indent: U32, strict: Bool, expand: Bool}
19
20def ddefaults() -> DOpts:
21  DOpts{2, True{}, False{}}
22
23# Apply every line in order, stopping at the first error.
24def fold(+ls: List<&2, D.DL>, +sr: Result<&2, &2, String, F.St>, +strict: Bool) -> Result<&2, &2, String, F.St>:
25  match ls sr:
26    case Nil{} _:
27      sr
28    case Con{l, t} Done{st}:
29      fold(t, T.step(l, st, strict), strict)
30    case Con{l, t} Fail{e}:
31      Fail{e}
32
33def root.value(+r: Result<&2, &2, String, F.St>) -> Result<&2, &2, String, J.Json>:
34  match r:
35    case Fail{e}:
36      Fail{e}
37    case Done{F.St{stk, Some{v}, bmin}}:
38      Done{v}
39    case Done{st}:
40      Fail{"no value"}
41
42# The stack height: enough steps to close every frame.
43def fuel(+r: Result<&2, &2, String, F.St>) -> Nat:
44  match r:
45    case Done{F.St{stk, root, bmin}}:
46      List.length(&2, F.Fr, stk)
47    case Fail{e}:
48      0n
49
50# Close what is still open and take the root.
51def finish(+r: Result<&2, &2, String, F.St>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
52  root.value(F.pop(fuel(r), r, 0, strict))
53
54def all_blank(+ls: List<&2, D.DL>) -> Bool:
55  match ls:
56    case Nil{}:
57      True{}
58    case Con{D.DL{no, depth, text, blank}, t}:
59      blank && all_blank(t)
60
61def skip_blank(+ls: List<&2, D.DL>) -> List<&2, D.DL>:
62  match ls:
63    case Con{D.DL{no, depth, text, True{}}, t}:
64      skip_blank(t)
65    case _:
66      ls
67
68def init(+stk: List<&2, F.Fr>, +root: Maybe<&2, J.Json>) -> Result<&2, &2, String, F.St>:
69  Done{F.St{stk, root, S.none()}}
70
71def object.root() -> Result<&2, &2, String, F.St>:
72  init(Con{F.FObj{0, F.ARoot{}, J.JNil{}}, Nil{}}, None{})
73
74# The root is an array: its header started a value or an open frame.
75def array.root(+r: Result<&2, &2, String, T.Start>) -> Result<&2, &2, String, F.St>:
76  match r:
77    case Fail{e}:
78      Fail{e}
79    case Done{T.SVal{v}}:
80      init(Nil{}, Some{v})
81    case Done{T.SOpen{fr}}:
82      init(Con{fr, Nil{}}, None{})
83
84def run.object(+ls: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
85  finish(fold(ls, object.root(), strict), strict)
86
87def run.array(+ls: List<&2, D.DL>, +r: Result<&2, &2, String, T.Start>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
88  finish(fold(ls, array.root(r), strict), strict)
89
90# A one-line document that is no entry is a primitive.
91def single.pick(+r: Result<&2, &2, String, E.Ent>, +text: String, +ls: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
92  match r:
93    case Done{E.ENot{}}:
94      P.parse(text)
95    case _:
96      run.object(ls, strict)
97
98def single.if(+empty: Bool, +text: String, +ls: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
99  match empty:
100    case True{}:
101      Done{J.JArr{J.JNil{}}}
102    case False{}:
103      single.pick(E.parse(strict, text), text, ls, strict)
104
105def single(+text: String, +ls: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
106  single.if(String.eq(text, "[]"), text, ls, strict)
107
108# Section 5 root form discovery, after the header of a root array failed.
109def root.other(+one: Bool, +text: String, +ls: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
110  match one:
111    case True{}:
112      single(text, ls, strict)
113    case False{}:
114      run.object(ls, strict)
115
116def root.hdr(+r: Result<&2, &2, String, H.Hdr>, +one: Bool, +text: String, +all: List<&2, D.DL>, +rest: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
117  match r:
118    case Done{h}:
119      run.array(rest, T.arr_start(F.ARoot{}, 1, h, strict), strict)
120    case Fail{e}:
121      root.other(one, text, all, strict)
122
123def header.try.pick(+root: Bool, +text: String) -> Result<&2, &2, String, H.Hdr>:
124  match root:
125    case True{}:
126      H.parse(text)
127    case False{}:
128      Fail{"not a root header"}
129
130def header.try(+depth: U32, +text: String) -> Result<&2, &2, String, H.Hdr>:
131  header.try.pick(U32.is_zero(depth) && Hd.lead(text, 91), text)
132
133def start.first(+l: D.DL, +rest: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
134  match l:
135    case D.DL{no, depth, text, blank}:
136      root.hdr(header.try(depth, text), all_blank(rest), text, Con{l, rest}, rest, strict)
137
138def start.skipped(+ls: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
139  match ls:
140    case Nil{}:
141      Done{J.JObj{J.JNil{}}}
142    case Con{l, rest}:
143      start.first(l, rest, strict)
144
145def start(+ls: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
146  start.skipped(skip_blank(ls), strict)
147
148def run(+r: Result<&2, &2, String, List<&2, D.DL>>, +strict: Bool) -> Result<&2, &2, String, J.Json>:
149  match r:
150    case Fail{e}:
151      Fail{e}
152    case Done{ls}:
153      start(ls, strict)
154
155def post.pick(+expand: Bool, +strict: Bool, +v: J.Json) -> Result<&2, &2, String, J.Json>:
156  match expand:
157    case True{}:
158      X.expand(v, strict)
159    case False{}:
160      Done{X.strip(v)}
161
162# Quoted-key markers come off; with path expansion the dotted keys unfold.
163def post(+expand: Bool, +strict: Bool, +r: Result<&2, &2, String, J.Json>) -> Result<&2, &2, String, J.Json>:
164  match r:
165    case Fail{e}:
166      Fail{e}
167    case Done{v}:
168      post.pick(expand, strict, v)
169
170# Decode TOON text.
171def decode(+src: String, +o: DOpts) -> Result<&2, &2, String, J.Json>:
172  match o:
173    case DOpts{indent, strict, expand}:
174      post(expand, strict, run(D.prep(src, indent, strict), strict))