lib.bend source
lib.bend on the hub · documented module
# Json v1 — leaf-only JSON AST + encode/decode for Bend.# Publish entry for this package. Depends only on Base (no Parse hub import).## Number representation: U32 (unsigned 32-bit integers). No floats, signs, or# exponents in v1.## AST constructors: JNull | JBool | JNum(U32) | JStr.# Arrays/objects (nested encode/decode) are deferred to json-nested / v2.## Supported decode subset (leaves only):# - null, true, false# - numbers: one or more decimal digits → U32 (via U32.read)# - strings: "..." with escapes \", \\, \/, \n, \t, \r (other \x fail)# - optional JSON whitespace (space/tab/CR/LF) around a single leaf value# Not in v1: arrays, objects, floats, negatives, unicode \uXXXX.import Basetype Json is Data: JNull{} JBool{b: Bool} JNum{n: U32} JStr{s: String}# ---- constructors / sugar -------------------------------------------------def Json.null() -> Json: JNull{}def Json.bool(b: Bool) -> Json: JBool{b}def Json.num(n: U32) -> Json: JNum{n}def Json.str(s: String) -> Json: JStr{s}# ---- encode / show --------------------------------------------------------def Json.encode.bool(b: Bool) -> String: match b: case False{}: "false" case True{}: "true"# Escape " and \ only when printing; other chars pass through (valid if already# JSON-safe). Decode accepts a slightly richer escape set (see decode.str).def Json.encode.esc.char(+c: Char) -> String: match c: case Chr{+x}: Bool.pick(String, U32.is_eq(x, 34), "\\\"", Bool.pick(String, U32.is_eq(x, 92), "\\\\", SCon{Chr{x}, SNil{}}))def Json.encode.esc.go(s: String) -> String: match s: case SNil{}: SNil{} case SCon{+h, t}: String.append(Json.encode.esc.char(h), Json.encode.esc.go(t))def Json.encode.str(+s: String) -> String: String.append("\"", String.append(Json.encode.esc.go(s), "\""))def Json.encode(j: Json) -> String: match j: case JNull{}: "null" case JBool{b}: Json.encode.bool(b) case JNum{n}: U32.show(n) case JStr{s}: Json.encode.str(s)def Json.show(j: Json) -> String: Json.encode(j)# ---- decode helpers (Base-only mini parser; not the Parse package) --------def Json.skip_ws(s: String) -> String: String.trim_start(s)def Json.starts(+p: String, +s: String) -> Bool: String.starts_with(s, p)def Json.drop(+n: Nat, s: String) -> String: String.drop(s, n)# Span leading digits. Nat fuel + step flag (same shape as Parse.take_while).def Json.take_digits.go( n: Nat, s: String, acc: String, step: Bool, +h: Char, ok: Bool) -> String & String: match n s step ok: case 0n s2 _ _: (String.reverse(acc), s2) case 1n+p SNil{} False{} _: (String.reverse(acc), SNil{}) case 1n+p SCon{+h2, t} False{} _: Json.take_digits.go(p, t, acc, True{}, h2, Char.is_digit(h2)) case 1n+p s2 True{} False{}: (String.reverse(acc), SCon{h, s2}) case 1n+p s2 True{} True{}: Json.take_digits.go(p, s2, SCon{h, acc}, False{}, h, False{}) case 1n+p s2 False{} _: (String.reverse(acc), s2)def Json.take_digits(+s: String) -> String & String: Json.take_digits.go( Nat.add(String.length(s), String.length(s)), s, SNil{}, False{}, Char.from_u32(0), False{} )# String body with escapes. Nat fuel (2 steps/char) + esc flag; +fuel clones# Nat for Bool.pick arms (affine otherwise).def Json.decode.str.go( n: Nat, esc: Bool, s: String, +acc: String) -> Maybe<&1, String & String>: match n esc s: case 0n _ _: None{} case 1n+p False{} SNil{}: None{} case 1n+p False{} SCon{Chr{+x}, +t}: +fuel = p Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 34), Some{(String.reverse(acc), t)}, Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 92), Json.decode.str.go(fuel, True{}, t, acc), Json.decode.str.go(fuel, False{}, t, SCon{Chr{x}, acc}))) case 1n+p True{} SNil{}: None{} case 1n+p True{} SCon{Chr{+x}, +t}: +fuel = p Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 34), Json.decode.str.go(fuel, False{}, t, SCon{Chr{34}, acc}), Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 92), Json.decode.str.go(fuel, False{}, t, SCon{Chr{92}, acc}), Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 47), Json.decode.str.go(fuel, False{}, t, SCon{Chr{47}, acc}), Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 110), Json.decode.str.go(fuel, False{}, t, SCon{Chr{10}, acc}), Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 116), Json.decode.str.go(fuel, False{}, t, SCon{Chr{9}, acc}), Bool.pick(Maybe<&1, String & String>, U32.is_eq(x, 114), Json.decode.str.go(fuel, False{}, t, SCon{Chr{13}, acc}), None{}))))))def Json.decode.str.after_quote(+s: String) -> Maybe<&1, String & String>: Json.decode.str.go( Nat.add(String.length(s), String.length(s)), False{}, s, SNil{} )def Json.decode.lit(+rest: String, v: Json, +s: String) -> Maybe<&1, Json & String>: Bool.pick( Maybe<&1, Json & String>, Json.starts(rest, s), Some{(v, Json.drop(String.length(rest), s))}, None{} )def Json.decode.string_val.cont( m: Maybe<&1, String & String>) -> Maybe<&1, Json & String>: match m: case None{}: None{} case Some{(body, rest)}: Some{(JStr{body}, rest)}def Json.decode.string_val(s: String) -> Maybe<&1, Json & String>: Json.decode.string_val.cont(Json.decode.str.after_quote(s))def Json.decode.number.read( rest: String, m: Maybe<&2, U32>) -> Maybe<&1, Json & String>: match m: case None{}: None{} case Some{n}: Some{(JNum{n}, rest)}def Json.decode.number.go( pair: String & String) -> Maybe<&1, Json & String>: (digits, rest) = pair match digits: case SNil{}: None{} case SCon{h, t}: Json.decode.number.read(rest, U32.read(SCon{h, t}))def Json.decode.number(s: String) -> Maybe<&1, Json & String>: Json.decode.number.go(Json.take_digits(s))# Leaf value only (no '[' / '{').def Json.decode.value(s: String) -> Maybe<&1, Json & String>: match s: case SNil{}: None{} case SCon{Chr{+x}, +t}: Bool.pick(Maybe<&1, Json & String>, U32.is_eq(x, 110), Json.decode.lit("ull", JNull{}, t), Bool.pick(Maybe<&1, Json & String>, U32.is_eq(x, 116), Json.decode.lit("rue", JBool{True{}}, t), Bool.pick(Maybe<&1, Json & String>, U32.is_eq(x, 102), Json.decode.lit("alse", JBool{False{}}, t), Bool.pick(Maybe<&1, Json & String>, U32.is_eq(x, 34), Json.decode.string_val(t), Bool.pick(Maybe<&1, Json & String>, Char.is_digit(Chr{x}), Json.decode.number(SCon{Chr{x}, t}), None{})))))def Json.decode.finish.rest(v: Json, rest: String) -> Maybe<&2, Json>: match rest: case SNil{}: Some{v} case SCon{h, t}: None{}def Json.decode.finish.go(m: Maybe<&1, Json & String>) -> Maybe<&2, Json>: match m: case None{}: None{} case Some{(v, rest)}: Json.decode.finish.rest(v, Json.skip_ws(rest))def Json.decode.finish(m: Maybe<&1, Json & String>) -> Maybe<&2, Json>: Json.decode.finish.go(m)def Json.decode(s: String) -> Maybe<&2, Json>: Json.decode.finish(Json.decode.value(Json.skip_ws(s)))def Json.parse(s: String) -> Maybe<&2, Json>: Json.decode(s)def Json.decode_prefix(s: String) -> Maybe<&1, Json & String>: Json.decode.value(Json.skip_ws(s))