From cdc6037af740566d5329cef9d303f06e81682780 Mon Sep 17 00:00:00 2001 From: Bruce Hill Date: Sun, 17 Aug 2025 13:33:23 -0400 Subject: Add JSON module. --- lib/README.md | 1 + lib/json/CHANGES.md | 5 ++ lib/json/README.md | 16 +++++ lib/json/json.tm | 173 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 195 insertions(+) create mode 100644 lib/json/CHANGES.md create mode 100644 lib/json/README.md create mode 100644 lib/json/json.tm (limited to 'lib') diff --git a/lib/README.md b/lib/README.md index 9b73c798..31de5b92 100644 --- a/lib/README.md +++ b/lib/README.md @@ -10,6 +10,7 @@ Libraries can be installed with `tomo -IL ./library-folder` - [base64](base64/): A base64 encoding/decoding library. - [commands](commands/): A library for running commands. - [core](core/): Bundling up commonly used libraries into a single library. +- [json](json/): JSON parsing and encoding. - [patterns](patterns/): Pattern matching for text. - [pthreads](pthreads/): A POSIX threads library. - [random](random/): Pseudorandom number generators. diff --git a/lib/json/CHANGES.md b/lib/json/CHANGES.md new file mode 100644 index 00000000..42ae752c --- /dev/null +++ b/lib/json/CHANGES.md @@ -0,0 +1,5 @@ +# Version History + +## v1.0 + +Initial version diff --git a/lib/json/README.md b/lib/json/README.md new file mode 100644 index 00000000..eb0d6c88 --- /dev/null +++ b/lib/json/README.md @@ -0,0 +1,16 @@ +# JSON + +This is a library for encoding/decoding JSON values. + +## Usage + +```tomo +j := JSON.Object({"key": Number(1.5), "key": Array([Boolean(yes), Null])}) +say("$(j.encode())") +say("$(j.pretty_print())") + +when JSON.parse("[1, null, true]") is Success(obj) + >> obj +is Failure(msg) + fail("Failed to parse JSON: $msg") +``` diff --git a/lib/json/json.tm b/lib/json/json.tm new file mode 100644 index 00000000..8127ce52 --- /dev/null +++ b/lib/json/json.tm @@ -0,0 +1,173 @@ +# Base 64 encoding and decoding +use patterns + +enum JSONDecodeResult( + Success(json:JSON) + Failure(reason:Text) +) + func invalid(text:Text -> JSONDecodeResult) + return Failure("Unrecognized JSON: $(text.quoted())") + +extend Text + func json_quoted(text:Text -> Text) + return '"' ++ text.translate({ + "\\"="\\\\", + '"'='\\"', + "\f"="\\f", + "\r"="\\r", + "\n"="\\n", + "\b"="\\b", + "\t"="\\t", + }) ++ '"' + +enum JSON( + Object(items:{Text=JSON}) + Array(items:[JSON]) + Boolean(value:Bool) + String(text:Text) + Number(n:Num) + Null +) + func encode(j:JSON -> Text) + when j is Object(items) + return "{" ++ ", ".join([ + '$(k.json_quoted()): $(v.encode())' + for k,v in items + ]) ++ "}" + is Array(items) + return "[" ++ ", ".join([item.encode() for item in items]) ++ "]" + is Boolean(value) + return (if value then "true" else "false") + is String(text) + return text.json_quoted() + is Number(n) + return "$n" + is Null + return "null" + + func pretty_print(j:JSON, max_line:Int=80, indent:Text=" ", current_indent:Text="" -> Text) + inline := j.encode() + if inline.length > max_line + next_indent := current_indent ++ indent + when j is Object(items) + return "{\n$next_indent" ++ ",\n$next_indent".join([ + '$(k.json_quoted()): $(v.pretty_print(max_line, indent, next_indent))' + for k,v in items + ]) ++ "\n$current_indent}" + is Array(items) + return "[\n$next_indent" ++ ",\n$next_indent".join([item.pretty_print(max_line, indent, next_indent) for item in items]) ++ "\n$current_indent]" + else pass + + return inline + + func parse_text(text:Text, remainder:&Text? = none -> JSONDecodeResult) + if text.starts_with('"') + string := "" + pos := 2 + escapes := {"n"="\n", "t"="\t", "r"="\r", '"'='"', "\\"="\\", "/"="/", "b"="\b", "f"="\f"} + while pos <= text.length + c := text[pos] + if c == '"' + if remainder + remainder[] = text.from(pos + 1) + return Success(JSON.String(string)) + + if c == "\\" + stop if pos + 1 > text.length + + if esc := escapes[text[pos+1]] + string ++= esc + pos += 2 + else if m := text.matching_pattern($Pat/u{4 digit}/) + string ++= Text.from_codepoints([Int32.parse(m.captures[1])!]) + pos += 1 + m.text.length + else + if remainder + remainder[] = text + return JSONDecodeResult.invalid(text) + else + string ++= c + pos += 1 + + if remainder + remainder[] = text + return JSONDecodeResult.invalid(text) + + func parse(text:Text, remainder:&Text? = none, trailing_commas:Bool=no -> JSONDecodeResult) + if text.starts_with("true", remainder) + return Success(JSON.Boolean(yes)) + else if text.starts_with("false", remainder) + return Success(JSON.Boolean(no)) + else if text.starts_with("null", remainder) + return Success(JSON.Null) + else if n := Num.parse(text, remainder) + return Success(JSON.Number(n)) + else if text.starts_with('"') + return JSON.parse_text(text, remainder) + else if text.starts_with("[") + elements : &[JSON] + text = text.from(2).trim_pattern($Pat"{whitespace}", right=no) + repeat + when JSON.parse(text, &text) is Success(elem) + elements.insert(elem) + else stop + + if delim := text.matching_pattern($Pat'{0+ ws},{0+ ws}') + text = text.from(delim.text.length + 1) + else stop + + if trailing_commas + if delim := text.matching_pattern($Pat'{0+ ws},{0+ ws}') + text = text.from(delim.text.length + 1) + + if terminator := text.matching_pattern($Pat'{0+ ws}]') + if remainder + remainder[] = text.from(terminator.text.length + 1) + return Success(JSON.Array(elements)) + else if text.starts_with("{") + object : &{Text=JSON} + text = text.from(2).trim_pattern($Pat"{whitespace}", right=no) + repeat + key_text := text + when JSON.parse_text(text, &text) is Success(key) + if separator := text.matching_pattern($Pat'{0+ ws}:{0+ ws}') + text = text.from(separator.text.length + 1) + else + return JSONDecodeResult.invalid(text) + + when JSON.parse(text, &text) is Success(value) + when key is String(str) + object[str] = value + else + return JSONDecodeResult.invalid(key_text) + else + return JSONDecodeResult.invalid(text) + else stop + + if delim := text.matching_pattern($Pat'{0+ ws},{0+ ws}') + text = text.from(delim.text.length + 1) + else stop + + if trailing_commas + if delim := text.matching_pattern($Pat'{0+ ws},{0+ ws}') + text = text.from(delim.text.length + 1) + + if terminator := text.matching_pattern($Pat'{0+ ws}{}}') + if remainder + remainder[] = text.from(terminator.text.length + 1) + return Success(JSON.Object(object)) + + return JSONDecodeResult.invalid(text) + +func main(input=(/dev/stdin), pretty_print:Bool = no, trailing_commas:Bool = yes) + text := (input.read() or exit("Invalid file: $input")).trim_pattern($Pat"{whitespace}") + while text.length > 0 + when JSON.parse(text, remainder=&text, trailing_commas=trailing_commas) is Success(json) + if pretty_print + say(json.pretty_print()) + else + say(json.encode()) + is Failure(msg) + exit("\033[31;1m$msg\033[m", code=1) + + text = text.trim_pattern($Pat"{whitespace}") -- cgit v1.2.3