plum
git clone https://git.pyrossh.dev/plum
A statically typed, imperative programming language inspired by rust, python
plum-std/Json.plum
| 141de54 | 1 | module std |
| 141de54 | 2 | |
| b7071c9 | 3 | import std/Option |
| b7071c9 | 4 | import std/Result |
| b7071c9 | 5 | import std/List |
| b7071c9 | 6 | import std/Map |
| ca5fd6f | 7 | import std/Number |
| b7071c9 | 8 | import std/Str |
| b7071c9 | 9 | import std/Buffer |
| b7071c9 | 10 | import std/Err |
| 73b5e55 | 11 | import std/Bool |
| 5de508e | 12 | |
| a43f3af | 13 | # A parsed JSON value. Numbers are always stored as `Float` (JSON doesn't |
| a43f3af | 14 | # distinguish integers from floats the way Plum does), and `JsonList`/ |
| a43f3af | 15 | # `JsonMap` nest `Json` itself — the same self-referential shape `List[T]` |
| a43f3af | 16 | # already uses internally for `Node[T].next: Option[Node[T]]`. |
| 5de508e | 17 | enum Json = |
| a43f3af | 18 | | JsonNull |
| 287b97c | 19 | | JsonBool(Bool) |
| 287b97c | 20 | | JsonFloat(Float) |
| 287b97c | 21 | | JsonStr(Str) |
| 287b97c | 22 | | JsonList(List[Json]) |
| 287b97c | 23 | | JsonMap(Map[Str, Json]) |
| 141de54 | 24 | |
| a43f3af | 25 | # Parses `s` as a single JSON document, requiring the whole string (aside |
| a43f3af | 26 | # from surrounding whitespace) to be consumed. Called as `Json.fromStr(s)`, |
| a43f3af | 27 | # matching `Int.fromStr`/`Float.fromStr`'s naming. |
| a43f3af | 28 | fun fromStr(s: Str) -> Result[Json, JsonParseError] = |
| a43f3af | 29 | parser := JsonParser(src: s, pos: 0) |
| a43f3af | 30 | parser.skipSpace() |
| a43f3af | 31 | r := parser.parseValue() |
| a43f3af | 32 | match r |
| a43f3af | 33 | Err(e) => |
| a43f3af | 34 | return Err(e) |
| a43f3af | 35 | Ok(v) => |
| a43f3af | 36 | parser.skipSpace() |
| a43f3af | 37 | if parser.pos < parser.src.length() |
| a43f3af | 38 | return Err(parser.fail("unexpected trailing characters")) |
| a43f3af | 39 | return Ok(v) |
| a43f3af | 40 | |
| a43f3af | 41 | # Renders `self` back to its JSON text form. A literal `{`/`}` is written |
| a43f3af | 42 | # doubled (`{{`/`}}`) — a single, un-doubled brace in a Plum string starts |
| a43f3af | 43 | # or ends a `{expr}` interpolation instead of standing for itself. |
| a43f3af | 44 | fun toStr(self) -> Str = |
| a43f3af | 45 | match self |
| 3a2119e | 46 | JsonNull => "null" |
| 3a2119e | 47 | JsonBool(b) => b ? "true" : "false" |
| 3a2119e | 48 | JsonFloat(f) => f.toStr() |
| 3a2119e | 49 | JsonStr(s) => "\"" + escapeJsonStr(s) + "\"" |
| 3a2119e | 50 | JsonList(items) => "[" + items.join(",") + "]" |
| 3a2119e | 51 | JsonMap(pairs) => "{{" + jsonMapToStr(pairs) + "}}" |
| a43f3af | 52 | |
| a43f3af | 53 | fun isNull(self) -> Bool = |
| a43f3af | 54 | match self |
| 3a2119e | 55 | JsonNull => True |
| 3a2119e | 56 | _ => False |
| a43f3af | 57 | |
| a43f3af | 58 | fun asBool(self) -> Option[Bool] = |
| a43f3af | 59 | match self |
| 3a2119e | 60 | JsonBool(b) => Some(b) |
| 3a2119e | 61 | _ => None |
| a43f3af | 62 | |
| a43f3af | 63 | fun asFloat(self) -> Option[Float] = |
| a43f3af | 64 | match self |
| 3a2119e | 65 | JsonFloat(f) => Some(f) |
| 3a2119e | 66 | _ => None |
| a43f3af | 67 | |
| a43f3af | 68 | fun asStr(self) -> Option[Str] = |
| a43f3af | 69 | match self |
| 3a2119e | 70 | JsonStr(s) => Some(s) |
| 3a2119e | 71 | _ => None |
| a43f3af | 72 | |
| a43f3af | 73 | fun asList(self) -> Option[List[Json]] = |
| a43f3af | 74 | match self |
| 3a2119e | 75 | JsonList(items) => Some(items) |
| 3a2119e | 76 | _ => None |
| a43f3af | 77 | |
| a43f3af | 78 | fun asMap(self) -> Option[Map[Str, Json]] = |
| a43f3af | 79 | match self |
| 3a2119e | 80 | JsonMap(pairs) => Some(pairs) |
| 3a2119e | 81 | _ => None |
| a43f3af | 82 | |
| a43f3af | 83 | # Carries where in the source a parse failure happened, alongside the |
| a43f3af | 84 | # message — implements the shared `Err` trait (`libs/std/err.plum`) rather |
| a43f3af | 85 | # than being a plain `Str`, so a caller that wants the position can get it |
| a43f3af | 86 | # without re-parsing the message. |
| 5f2f962 | 87 | enum JsonParseError(Err) = |
| 5f2f962 | 88 | | JsonParseError(pos: Int, text: Str) |
| a43f3af | 89 | |
| a43f3af | 90 | fun code(self) -> Int = |
| a43f3af | 91 | 1 |
| a43f3af | 92 | |
| a43f3af | 93 | fun msg(self) -> Str = |
| a43f3af | 94 | "json parse error at position {self.pos}: {self.text}" |
| a43f3af | 95 | |
| a43f3af | 96 | # A bare payload-free `None` here can't be disambiguated to one specific |
| a43f3af | 97 | # `Option` instantiation from this expression alone (see the README's |
| a43f3af | 98 | # "Known gaps" note on this residual generics limitation — unrelated to the |
| a43f3af | 99 | # 3 points fixed above) — an explicit `Option[JsonParseError]` return type |
| a43f3af | 100 | # sidesteps it. |
| a43f3af | 101 | fun cause(self) -> Option[JsonParseError] = |
| a43f3af | 102 | None |
| e502e58 | 103 | |
| a43f3af | 104 | # A recursive-descent parser over a `Str` source, tracking its read position |
| a43f3af | 105 | # in `pos`. (The original design threaded a `Readable` stream through here, |
| a43f3af | 106 | # but `Readable` is an undefined trait with no methods anywhere in `libs/std` |
| a43f3af | 107 | # — there is nothing to call on it — so this reads directly from a `Str` |
| a43f3af | 108 | # instead, the same way every other stdlib parser, e.g. `Int.fromStr`, does.) |
| 5f2f962 | 109 | enum JsonParser = |
| 5f2f962 | 110 | | JsonParser(src: Str, pos: Int) |
| 5de508e | 111 | |
| a43f3af | 112 | fun fail(self, text: Str) -> JsonParseError = |
| a43f3af | 113 | JsonParseError(pos: self.pos, text: text) |
| a43f3af | 114 | |
| a43f3af | 115 | # The raw byte at the current position, or 0 (never a valid JSON byte) past |
| a43f3af | 116 | # the end of `src` — lets every caller test for "end of input" the same way |
| a43f3af | 117 | # it tests for any other byte, with no separate bounds check. |
| a43f3af | 118 | fun peek(self) -> Int = |
| a43f3af | 119 | self.pos >= self.src.length() ? 0 : self.src.byteAt(self.pos) |
| a43f3af | 120 | |
| a43f3af | 121 | fun peekAt(self, offset: Int) -> Int = |
| a43f3af | 122 | i := self.pos + offset |
| a43f3af | 123 | i >= self.src.length() ? 0 : self.src.byteAt(i) |
| a43f3af | 124 | |
| a43f3af | 125 | fun advance(self) -> Unit = |
| a43f3af | 126 | self.pos = self.pos + 1 |
| a43f3af | 127 | |
| a43f3af | 128 | fun skipSpace(self) -> Unit = |
| a43f3af | 129 | while isSpace(self.peek()) |
| a43f3af | 130 | self.advance() |
| a43f3af | 131 | |
| a43f3af | 132 | fun parseValue(self) -> Result[Json, JsonParseError] = |
| a43f3af | 133 | self.skipSpace() |
| a43f3af | 134 | c := self.peek() |
| a43f3af | 135 | if c == 34 |
| a43f3af | 136 | return self.parseString() |
| a43f3af | 137 | if c == 91 |
| a43f3af | 138 | return self.parseArray() |
| a43f3af | 139 | if c == 123 |
| a43f3af | 140 | return self.parseObject() |
| a43f3af | 141 | if c == 116 |
| a43f3af | 142 | return self.parseLiteral("true", JsonBool(True)) |
| a43f3af | 143 | if c == 102 |
| a43f3af | 144 | return self.parseLiteral("false", JsonBool(False)) |
| a43f3af | 145 | if c == 110 |
| a43f3af | 146 | return self.parseLiteral("null", JsonNull) |
| a43f3af | 147 | if c == 45 || isDigit(c) |
| a43f3af | 148 | return self.parseNumber() |
| a43f3af | 149 | return Err(self.fail("unexpected character")) |
| a43f3af | 150 | |
| a43f3af | 151 | fun parseLiteral(self, lit: Str, value: Json) -> Result[Json, JsonParseError] = |
| a43f3af | 152 | n := lit.length() |
| a43f3af | 153 | if !matchesAt(self.src, lit, self.pos, n) |
| a43f3af | 154 | return Err(self.fail("invalid literal, expected '{lit}'")) |
| a43f3af | 155 | self.pos = self.pos + n |
| a43f3af | 156 | return Ok(value) |
| a43f3af | 157 | |
| a43f3af | 158 | fun parseNumber(self) -> Result[Json, JsonParseError] = |
| a43f3af | 159 | start := self.pos |
| a43f3af | 160 | if self.peek() == 45 |
| a43f3af | 161 | self.advance() |
| a43f3af | 162 | if !isDigit(self.peek()) |
| a43f3af | 163 | return Err(self.fail("invalid number")) |
| a43f3af | 164 | while isDigit(self.peek()) |
| a43f3af | 165 | self.advance() |
| a43f3af | 166 | if self.peek() == 46 |
| a43f3af | 167 | self.advance() |
| a43f3af | 168 | if !isDigit(self.peek()) |
| a43f3af | 169 | return Err(self.fail("invalid number")) |
| a43f3af | 170 | while isDigit(self.peek()) |
| a43f3af | 171 | self.advance() |
| a43f3af | 172 | if self.peek() == 101 || self.peek() == 69 |
| a43f3af | 173 | self.advance() |
| a43f3af | 174 | if self.peek() == 43 || self.peek() == 45 |
| a43f3af | 175 | self.advance() |
| a43f3af | 176 | if !isDigit(self.peek()) |
| a43f3af | 177 | return Err(self.fail("invalid number")) |
| a43f3af | 178 | while isDigit(self.peek()) |
| a43f3af | 179 | self.advance() |
| a43f3af | 180 | return self.parseNumberText(self.src.sub(start, self.pos)) |
| a43f3af | 181 | |
| a43f3af | 182 | # `Float.fromStr` (`libs/std/float.plum`) doesn't understand exponent |
| a43f3af | 183 | # notation, so a number that has one is split into its mantissa and |
| a43f3af | 184 | # exponent around the `e`/`E` and recombined here instead. |
| a43f3af | 185 | fun parseNumberText(self, text: Str) -> Result[Json, JsonParseError] = |
| a43f3af | 186 | e_index := exponentIndex(text) |
| a43f3af | 187 | if e_index < 0 |
| ca5fd6f | 188 | r := parseFloat(text) |
| a43f3af | 189 | match r |
| a43f3af | 190 | Ok(f) => |
| a43f3af | 191 | return Ok(JsonFloat(f)) |
| a43f3af | 192 | Err(m) => |
| a43f3af | 193 | return Err(self.fail(m)) |
| ca5fd6f | 194 | mantissa := parseFloat(text.sub(0, e_index)) |
| ca5fd6f | 195 | exponent := parseInt(text.sub(e_index + 1, text.length())) |
| a43f3af | 196 | match mantissa |
| a43f3af | 197 | Err(m) => |
| a43f3af | 198 | return Err(self.fail(m)) |
| a43f3af | 199 | Ok(mv) => |
| a43f3af | 200 | match exponent |
| a43f3af | 201 | Err(m) => |
| a43f3af | 202 | return Err(self.fail(m)) |
| a43f3af | 203 | Ok(ev) => |
| a43f3af | 204 | return Ok(JsonFloat(mv * 10.pow(Float(ev)))) |
| a43f3af | 205 | |
| a43f3af | 206 | fun parseString(self) -> Result[Json, JsonParseError] = |
| a43f3af | 207 | self.advance() |
| 4a2384c | 208 | return self.parseStringBody(Buffer()) |
| a43f3af | 209 | |
| a43f3af | 210 | fun parseStringBody(self, buf: Buffer) -> Result[Json, JsonParseError] = |
| a43f3af | 211 | c := self.peek() |
| a43f3af | 212 | if c == 0 |
| a43f3af | 213 | return Err(self.fail("unterminated string")) |
| a43f3af | 214 | if c == 34 |
| a43f3af | 215 | self.advance() |
| a43f3af | 216 | return Ok(JsonStr(buf.toStr())) |
| a43f3af | 217 | if c == 92 |
| a43f3af | 218 | return self.parseEscape(buf) |
| a43f3af | 219 | n := self.src.codePointByteLength(self.pos) |
| a43f3af | 220 | buf.write(self.src.sub(self.pos, self.pos + n)) |
| a43f3af | 221 | self.pos = self.pos + n |
| a43f3af | 222 | return self.parseStringBody(buf) |
| 141de54 | 223 | |
| a43f3af | 224 | fun parseEscape(self, buf: Buffer) -> Result[Json, JsonParseError] = |
| a43f3af | 225 | self.advance() |
| a43f3af | 226 | esc := self.peek() |
| a43f3af | 227 | if esc == 34 |
| a43f3af | 228 | buf.write("\"") |
| a43f3af | 229 | self.advance() |
| a43f3af | 230 | return self.parseStringBody(buf) |
| a43f3af | 231 | if esc == 92 |
| a43f3af | 232 | buf.write("\\") |
| a43f3af | 233 | self.advance() |
| a43f3af | 234 | return self.parseStringBody(buf) |
| a43f3af | 235 | if esc == 47 |
| a43f3af | 236 | buf.write("/") |
| a43f3af | 237 | self.advance() |
| a43f3af | 238 | return self.parseStringBody(buf) |
| a43f3af | 239 | if esc == 98 |
| a43f3af | 240 | buf.write("\b") |
| a43f3af | 241 | self.advance() |
| a43f3af | 242 | return self.parseStringBody(buf) |
| a43f3af | 243 | if esc == 102 |
| a43f3af | 244 | buf.write("\f") |
| a43f3af | 245 | self.advance() |
| a43f3af | 246 | return self.parseStringBody(buf) |
| a43f3af | 247 | if esc == 110 |
| a43f3af | 248 | buf.write("\n") |
| a43f3af | 249 | self.advance() |
| a43f3af | 250 | return self.parseStringBody(buf) |
| a43f3af | 251 | if esc == 114 |
| a43f3af | 252 | buf.write("\r") |
| a43f3af | 253 | self.advance() |
| a43f3af | 254 | return self.parseStringBody(buf) |
| a43f3af | 255 | if esc == 116 |
| a43f3af | 256 | buf.write("\t") |
| a43f3af | 257 | self.advance() |
| a43f3af | 258 | return self.parseStringBody(buf) |
| a43f3af | 259 | if esc == 117 |
| a43f3af | 260 | self.advance() |
| a43f3af | 261 | return self.parseUnicodeEscape(buf) |
| a43f3af | 262 | return Err(self.fail("invalid escape character")) |
| 5de508e | 263 | |
| a43f3af | 264 | fun parseHex4(self) -> Result[Int, JsonParseError] = |
| a43f3af | 265 | return self.parseHex4From(0, 0) |
| 5de508e | 266 | |
| a43f3af | 267 | fun parseHex4From(self, value: Int, count: Int) -> Result[Int, JsonParseError] = |
| a43f3af | 268 | if count == 4 |
| a43f3af | 269 | return Ok(value) |
| a43f3af | 270 | d := hexDigitValue(self.peek()) |
| a43f3af | 271 | if d < 0 |
| a43f3af | 272 | return Err(self.fail("invalid unicode escape")) |
| a43f3af | 273 | self.advance() |
| a43f3af | 274 | return self.parseHex4From(value * 16 + d, count + 1) |
| 5de508e | 275 | |
| a43f3af | 276 | fun parseUnicodeEscape(self, buf: Buffer) -> Result[Json, JsonParseError] = |
| a43f3af | 277 | hr := self.parseHex4() |
| a43f3af | 278 | match hr |
| a43f3af | 279 | Err(e) => |
| a43f3af | 280 | return Err(e) |
| a43f3af | 281 | Ok(cp) => |
| a43f3af | 282 | return self.parseUnicodeEscapeCont(buf, cp) |
| 5de508e | 283 | |
| a43f3af | 284 | # A high surrogate (`0xD800`..`0xDBFF`) must be followed immediately by a |
| a43f3af | 285 | # low surrogate (`0xDC00`..`0xDFFF`) `\u` escape; the pair together encodes |
| a43f3af | 286 | # one codepoint above `0xFFFF`. Any other codepoint is used as-is. |
| a43f3af | 287 | fun parseUnicodeEscapeCont(self, buf: Buffer, cp: Int) -> Result[Json, JsonParseError] = |
| a43f3af | 288 | if cp >= 0xD800 && cp <= 0xDBFF && self.peek() == 92 && self.peekAt(1) == 117 |
| a43f3af | 289 | self.advance() |
| a43f3af | 290 | self.advance() |
| a43f3af | 291 | hr := self.parseHex4() |
| a43f3af | 292 | match hr |
| a43f3af | 293 | Err(e) => |
| a43f3af | 294 | return Err(e) |
| a43f3af | 295 | Ok(low) => |
| a43f3af | 296 | if low < 0xDC00 || low > 0xDFFF |
| a43f3af | 297 | return Err(self.fail("invalid low surrogate")) |
| a43f3af | 298 | combined := 0x10000 + {{cp - 0xD800} << 10} + {low - 0xDC00} |
| a43f3af | 299 | buf.writeRune(combined) |
| a43f3af | 300 | return self.parseStringBody(buf) |
| a43f3af | 301 | buf.writeRune(cp) |
| a43f3af | 302 | return self.parseStringBody(buf) |
| 5de508e | 303 | |
| a43f3af | 304 | fun parseArray(self) -> Result[Json, JsonParseError] = |
| a43f3af | 305 | self.advance() |
| a43f3af | 306 | self.skipSpace() |
| a43f3af | 307 | items := List[Json](head: None, tail: None, size: 0) |
| a43f3af | 308 | if self.peek() == 93 |
| a43f3af | 309 | self.advance() |
| a43f3af | 310 | return Ok(JsonList(items)) |
| a43f3af | 311 | return self.parseArrayItems(items) |
| 5de508e | 312 | |
| a43f3af | 313 | fun parseArrayItems(self, items: List[Json]) -> Result[Json, JsonParseError] = |
| a43f3af | 314 | self.skipSpace() |
| a43f3af | 315 | r := self.parseValue() |
| a43f3af | 316 | match r |
| a43f3af | 317 | Err(e) => |
| a43f3af | 318 | return Err(e) |
| a43f3af | 319 | Ok(v) => |
| a43f3af | 320 | items.add(v) |
| a43f3af | 321 | self.skipSpace() |
| a43f3af | 322 | c := self.peek() |
| a43f3af | 323 | if c == 44 |
| a43f3af | 324 | self.advance() |
| a43f3af | 325 | return self.parseArrayItems(items) |
| a43f3af | 326 | if c == 93 |
| a43f3af | 327 | self.advance() |
| a43f3af | 328 | return Ok(JsonList(items)) |
| a43f3af | 329 | return Err(self.fail("expected ',' or ']'")) |
| 5de508e | 330 | |
| a43f3af | 331 | fun parseObject(self) -> Result[Json, JsonParseError] = |
| a43f3af | 332 | self.advance() |
| a43f3af | 333 | self.skipSpace() |
| ea9b8c1 | 334 | pairs := Map[Str, Json]() |
| a43f3af | 335 | if self.peek() == 125 |
| a43f3af | 336 | self.advance() |
| a43f3af | 337 | return Ok(JsonMap(pairs)) |
| a43f3af | 338 | return self.parseObjectItems(pairs) |
| 5de508e | 339 | |
| a43f3af | 340 | fun parseObjectItems(self, pairs: Map[Str, Json]) -> Result[Json, JsonParseError] = |
| a43f3af | 341 | self.skipSpace() |
| a43f3af | 342 | if self.peek() != 34 |
| a43f3af | 343 | return Err(self.fail("expected string key")) |
| a43f3af | 344 | kr := self.parseString() |
| a43f3af | 345 | match kr |
| a43f3af | 346 | Err(e) => |
| a43f3af | 347 | return Err(e) |
| a43f3af | 348 | Ok(keyJson) => |
| a43f3af | 349 | return self.parseObjectValue(pairs, jsonStrValue(keyJson)) |
| a43f3af | 350 | |
| a43f3af | 351 | fun parseObjectValue(self, pairs: Map[Str, Json], key: Str) -> Result[Json, JsonParseError] = |
| a43f3af | 352 | self.skipSpace() |
| a43f3af | 353 | if self.peek() != 58 |
| a43f3af | 354 | return Err(self.fail("expected ':'")) |
| a43f3af | 355 | self.advance() |
| a43f3af | 356 | self.skipSpace() |
| a43f3af | 357 | vr := self.parseValue() |
| a43f3af | 358 | match vr |
| a43f3af | 359 | Err(e) => |
| a43f3af | 360 | return Err(e) |
| a43f3af | 361 | Ok(v) => |
| a43f3af | 362 | pairs.set(key, v) |
| a43f3af | 363 | self.skipSpace() |
| a43f3af | 364 | c := self.peek() |
| a43f3af | 365 | if c == 44 |
| a43f3af | 366 | self.advance() |
| a43f3af | 367 | return self.parseObjectItems(pairs) |
| a43f3af | 368 | if c == 125 |
| a43f3af | 369 | self.advance() |
| a43f3af | 370 | return Ok(JsonMap(pairs)) |
| a43f3af | 371 | return Err(self.fail("expected ',' or '}}'")) |
| a43f3af | 372 | |
| a43f3af | 373 | fun jsonStrValue(j: Json) -> Str = |
| a43f3af | 374 | match j |
| 3a2119e | 375 | JsonStr(s) => s |
| 3a2119e | 376 | _ => "" |
| a43f3af | 377 | |
| a43f3af | 378 | # `Map` has no built-in `toStr`/`join` (unlike `List`, which uses `join` for |
| a43f3af | 379 | # `Json.toStr`'s `JsonList` case above) — a plain free function taking the |
| a43f3af | 380 | # `Buffer` as a captured-by-reference parameter instead of a closure sidesteps |
| a43f3af | 381 | # `libs/std/list.plum`'s `join` note on closures: reassigning a CAPTURED |
| a43f3af | 382 | # variable inside a closure body doesn't persist across that closure's own |
| a43f3af | 383 | # repeated calls, but mutating a heap object's fields THROUGH a captured |
| a43f3af | 384 | # reference (as `appendJsonPair` does to `buf`) does. |
| a43f3af | 385 | fun jsonMapToStr(pairs: Map[Str, Json]) -> Str = |
| 4a2384c | 386 | buf := Buffer() |
| a43f3af | 387 | pairs.each(|k, v| appendJsonPair(buf, k, v)) |
| a43f3af | 388 | return buf.toStr() |
| a43f3af | 389 | |
| a43f3af | 390 | fun appendJsonPair(buf: Buffer, k: Str, v: Json) -> Unit = |
| a43f3af | 391 | if !buf.isEmpty() |
| a43f3af | 392 | buf.write(",") |
| a43f3af | 393 | buf.write("\"") |
| a43f3af | 394 | buf.write(escapeJsonStr(k)) |
| a43f3af | 395 | buf.write("\":") |
| a43f3af | 396 | buf.write(v.toStr()) |
| a43f3af | 397 | |
| a43f3af | 398 | fun exponentIndex(text: Str) -> Int = |
| a43f3af | 399 | e := text.indexOf("e") |
| a43f3af | 400 | e >= 0 ? e : text.indexOf("E") |
| a43f3af | 401 | |
| a43f3af | 402 | fun escapeJsonStr(s: Str) -> Str = |
| a43f3af | 403 | escapeJsonFrom(s, 0, s.length()) |
| a43f3af | 404 | |
| a43f3af | 405 | # Byte-at-a-time except for the pass-through case, which copies a whole |
| a43f3af | 406 | # UTF-8 codepoint at once — copying a lone continuation byte as its own |
| a43f3af | 407 | # "character" (as a naive byte-by-byte pass-through would) would corrupt any |
| a43f3af | 408 | # non-ASCII text, the same hazard documented on `Str`'s own `escapeChar` in |
| a43f3af | 409 | # `libs/std/str.plum`. |
| a43f3af | 410 | fun escapeJsonFrom(s: Str, i: Int, len: Int) -> Str = |
| a43f3af | 411 | if i >= len |
| a43f3af | 412 | return "" |
| a43f3af | 413 | b := s.byteAt(i) |
| a43f3af | 414 | if b == 34 |
| a43f3af | 415 | return "\\\"" + escapeJsonFrom(s, i + 1, len) |
| a43f3af | 416 | if b == 92 |
| a43f3af | 417 | return "\\\\" + escapeJsonFrom(s, i + 1, len) |
| a43f3af | 418 | if b == 10 |
| a43f3af | 419 | return "\\n" + escapeJsonFrom(s, i + 1, len) |
| a43f3af | 420 | if b == 13 |
| a43f3af | 421 | return "\\r" + escapeJsonFrom(s, i + 1, len) |
| a43f3af | 422 | if b == 9 |
| a43f3af | 423 | return "\\t" + escapeJsonFrom(s, i + 1, len) |
| a43f3af | 424 | if b == 8 |
| a43f3af | 425 | return "\\b" + escapeJsonFrom(s, i + 1, len) |
| a43f3af | 426 | if b == 12 |
| a43f3af | 427 | return "\\f" + escapeJsonFrom(s, i + 1, len) |
| a43f3af | 428 | if b < 0x20 |
| a43f3af | 429 | return "\\u00" + hexByteStr(b) + escapeJsonFrom(s, i + 1, len) |
| a43f3af | 430 | n := s.codePointByteLength(i) |
| a43f3af | 431 | return s.sub(i, i + n) + escapeJsonFrom(s, i + n, len) |
| a43f3af | 432 | |
| a43f3af | 433 | fun hexByteStr(b: Int) -> Str = |
| a43f3af | 434 | hexDigitChar(b / 16) + hexDigitChar(b % 16) |
| a43f3af | 435 | |
| a43f3af | 436 | fun hexDigitChar(d: Int) -> Str = |
| a43f3af | 437 | match d |
| a43f3af | 438 | 0 => "0" |
| a43f3af | 439 | 1 => "1" |
| a43f3af | 440 | 2 => "2" |
| a43f3af | 441 | 3 => "3" |
| a43f3af | 442 | 4 => "4" |
| a43f3af | 443 | 5 => "5" |
| a43f3af | 444 | 6 => "6" |
| a43f3af | 445 | 7 => "7" |
| a43f3af | 446 | 8 => "8" |
| a43f3af | 447 | 9 => "9" |
| a43f3af | 448 | 10 => "a" |
| a43f3af | 449 | 11 => "b" |
| a43f3af | 450 | 12 => "c" |
| a43f3af | 451 | 13 => "d" |
| a43f3af | 452 | 14 => "e" |
| a43f3af | 453 | 15 => "f" |
| a43f3af | 454 | _ => "0" |
| a43f3af | 455 | |
| a43f3af | 456 | fun hexDigitValue(b: Int) -> Int = |
| a43f3af | 457 | if b >= 48 && b <= 57 |
| a43f3af | 458 | return b - 48 |
| a43f3af | 459 | if b >= 97 && b <= 102 |
| a43f3af | 460 | return b - 97 + 10 |
| a43f3af | 461 | if b >= 65 && b <= 70 |
| a43f3af | 462 | return b - 65 + 10 |
| a43f3af | 463 | return -1 |
| 5de508e | 464 | |
| 0fe3528 | 465 | fun isSpace(c: Int) -> Bool = |
| 141de54 | 466 | c == 32 || c >= 9 && c <= 13 |
| 5de508e | 467 | |
| 0fe3528 | 468 | fun isDigit(c: Int) -> Bool = |
| 141de54 | 469 | c >= 48 && c <= 57 |
| a43f3af | 470 | |
| a43f3af | 471 | # ---- regression tests ---- |
| a43f3af | 472 | |
| a43f3af | 473 | fun parseFloatValue(s: Str) -> Float = |
| a43f3af | 474 | r := Json.fromStr(s) |
| a43f3af | 475 | match r |
| 3a2119e | 476 | Ok(v) => v.asFloat().unwrapOr(-1.0f) |
| 3a2119e | 477 | Err(_) => -1.0f |
| a43f3af | 478 | |
| a43f3af | 479 | test "parses null, true, and false" |
| a9a0147 | 480 | assert Json.fromStr("null").unwrap().isNull() |
| a9a0147 | 481 | assert Json.fromStr("true").unwrap().asBool().unwrap() |
| a9a0147 | 482 | assert !Json.fromStr("false").unwrap().asBool().unwrap() |
| a43f3af | 483 | |
| a43f3af | 484 | test "parses integers, decimals, negatives, and exponents" |
| a9a0147 | 485 | assert parseFloatValue("42") == 42.0f |
| a9a0147 | 486 | assert parseFloatValue("-7") == -7.0f |
| a9a0147 | 487 | assert parseFloatValue("3.5") == 3.5f |
| a9a0147 | 488 | assert parseFloatValue("1e2") == 100.0f |
| a9a0147 | 489 | assert parseFloatValue("1.5e-2") == 0.015f |
| a43f3af | 490 | |
| a43f3af | 491 | test "parses a plain string, with escapes" |
| a9a0147 | 492 | assert Json.fromStr("\"hello\"").unwrap().asStr().unwrap() == "hello" |
| a9a0147 | 493 | assert Json.fromStr("\"a\\nb\"").unwrap().asStr().unwrap() == "a\nb" |
| a9a0147 | 494 | assert Json.fromStr("\"\\u0041\"").unwrap().asStr().unwrap() == "A" |
| a43f3af | 495 | |
| a43f3af | 496 | test "parses an array of mixed values" |
| a43f3af | 497 | v := Json.fromStr("[1, \"two\", true, null]").unwrap() |
| a43f3af | 498 | items := v.asList().unwrap() |
| a9a0147 | 499 | assert items.length() == 4 |
| a9a0147 | 500 | assert items.get(0).unwrap().asFloat().unwrap() == 1.0f |
| a9a0147 | 501 | assert items.get(1).unwrap().asStr().unwrap() == "two" |
| a9a0147 | 502 | assert items.get(2).unwrap().asBool().unwrap() |
| a9a0147 | 503 | assert items.get(3).unwrap().isNull() |
| a43f3af | 504 | |
| a43f3af | 505 | test "parses a nested object" |
| a43f3af | 506 | v := Json.fromStr("{{\"a\": 1, \"b\": {{\"c\": [1, 2, 3]}}}}").unwrap() |
| a43f3af | 507 | pairs := v.asMap().unwrap() |
| a9a0147 | 508 | assert pairs.get("a").unwrap().asFloat().unwrap() == 1.0f |
| a43f3af | 509 | nested := pairs.get("b").unwrap().asMap().unwrap() |
| a43f3af | 510 | inner := nested.get("c").unwrap().asList().unwrap() |
| a9a0147 | 511 | assert inner.length() == 3 |
| a9a0147 | 512 | assert inner.get(2).unwrap().asFloat().unwrap() == 3.0f |
| a43f3af | 513 | |
| a43f3af | 514 | test "reports an error on malformed input" |
| a43f3af | 515 | r := Json.fromStr("{{\"a\": }}") |
| a9a0147 | 516 | assert r.isErr() |
| a43f3af | 517 | |
| a43f3af | 518 | test "round-trips a value through toStr and back" |
| a43f3af | 519 | original := "[1,\"two\",true,null,{{\"k\":3.5}}]" |
| a43f3af | 520 | v := Json.fromStr(original).unwrap() |
| a43f3af | 521 | again := Json.fromStr(v.toStr()).unwrap() |
| a9a0147 | 522 | assert again.toStr() == v.toStr() |
| a43f3af | 523 | |
| a43f3af | 524 | test "toStr escapes special characters in strings" |
| a43f3af | 525 | v := JsonStr("a\"b\\c\nd") |
| a9a0147 | 526 | assert v.toStr() == "\"a\\\"b\\\\c\\nd\"" |