1 import Base 2 import ./json.bend as J 3 import ./split.bend as S 4 import ./prim.bend as P 5 import ./hdr.bend as H 6 import ./entry.bend as E 7 import ./dline.bend as D 8 import ./frame.bend as F 9 import ./step.bend as T 10 import ./expand.bend as X 11 import ./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). 17 type DOpts is Data: 18 DOpts{indent: U32, strict: Bool, expand: Bool} 19 20 def ddefaults() -> DOpts: 21 DOpts{2, True{}, False{}} 22 23 # Apply every line in order, stopping at the first error. 24 def 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 33 def 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. 43 def 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. 51 def 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 54 def 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 61 def 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 68 def 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 71 def 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. 75 def 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 84 def run.object(+ls: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>: 85 finish(fold(ls, object.root(), strict), strict) 86 87 def 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. 91 def 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 98 def 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 105 def 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. 109 def 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 116 def 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 123 def 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 130 def 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 133 def 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 138 def 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 145 def start(+ls: List<&2, D.DL>, +strict: Bool) -> Result<&2, &2, String, J.Json>: 146 start.skipped(skip_blank(ls), strict) 147 148 def 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 155 def 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. 163 def 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. 171 def 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))