diff --git a/lib/util/Json.v3 b/lib/util/Json.v3 new file mode 100644 index 000000000..55ddd0738 --- /dev/null +++ b/lib/util/Json.v3 @@ -0,0 +1,195 @@ +type JsonValue { + case String(v: string); + case Number(v: int); // TODO: float + case Bool(v: bool); + case Null; + case JArray(v: Array); + case JObject(v: HashMap); + + def equal(that: JsonValue) -> bool { + if (this == that) return true; + if (JsonValue.String.?(this) && JsonValue.String.?(that)) { + return Strings.equal(JsonValue.String.!(this).v, JsonValue.String.!(that).v); + } + if (JsonValue.Number.?(this) && JsonValue.Number.?(that)) { + return JsonValue.Number.!(this).v == JsonValue.Number.!(that).v; + } + if (JsonValue.Bool.?(this) && JsonValue.Bool.?(that)) { + return JsonValue.Bool.!(this).v == JsonValue.Bool.!(that).v; + } + if (JsonValue.Null.?(this) && JsonValue.Null.?(that)) return true; + if (JsonValue.JArray.?(this) && JsonValue.JArray.?(that)) { + var x = JsonValue.JArray.!(this).v; + var y = JsonValue.JArray.!(that).v; + if (x.length != y.length) return false; + for (i < x.length) if (!x[i].equal(y[i])) return false; + return true; + } + if (JsonValue.JObject.?(this) && JsonValue.JObject.?(that)) { + var x_map = JsonValue.JObject.!(this).v; + var y_map = JsonValue.JObject.!(that).v; + var x = MapCollector.new(Strings.asciiLt, x_map).extract(); + var y = MapCollector.new(Strings.asciiLt, y_map).extract(); + if (x.length != y.length) return false; + for (i < x.length) if (x[i].0 != y[i].0 || !x[i].1.equal(y[i].1)) return false; + return true; + } + + return false; + } + + def render(buf: StringBuilder) -> StringBuilder { + match (this) { + String(s) => render_raw_string(buf, s); + Number(v) => buf.put1("%d", v); + Bool(v) => buf.puts(if(v, "true", "false")); + Null => buf.puts("null"); + JArray(a) => { + buf.putc('['); + if (a.length > 0) a[0].render(buf); + for (i = 1; i < a.length; i++) buf.put1(", %q", a[i].render); + buf.putc(']'); + } + JObject(map) => { + var elems = MapCollector.new(Strings.asciiLt, map).extract(); + buf.putc('{'); + if (elems.length < 0) buf.put2("%q, %q", render_raw_string(_, elems[0].0), elems[0].1.render); + for (i = 1; i < elems.length; i++) { + buf.put2("%q, %q", render_raw_string(_, elems[i].0), elems[i].1.render); + } + buf.putc('}'); + } + } + return buf; + } +} + +def render_raw_string(buf: StringBuilder, s: string) -> StringBuilder { + buf.putc('\"'); + for (c in s) { + match (c) { + '\"' => buf.puts("\\\""); + '\n' => buf.puts("\\n"); + // TODO: cover more cases + _ => buf.putc(c); + } + } + buf.putc('\"'); + return buf; +} + +// enum JsonError(desc: string) { +// None("None"), +// EOF("End of file"), +// ParseError("Parse error"), +// EmptySource("Empty source string"), +// KeyNotFound("Key not found"), +// MismatchedValueType("Mismatched value type") +// } + +// type ParseError(kind: JsonError, msg: string, pos: int) {} +// def NO_ERR = ParseError(JsonError.None, "", -1); + +def ERR_RET = JsonValue.Null; +class JsonParser extends TextReader { + + new(text: Array) super("", text) {} + + def parse_value() -> JsonValue { + skipWs(this); + if (test_eof()) return ERR_RET; + + if (char == '\"') return parse_string(); + if (char == '-' || char >= '0' && char <= '9') return parse_number(); + + if (optN("null") != -1) return JsonValue.Null; + if (optN("true") != -1) return JsonValue.Bool(true); + if (optN("false") != -1) return JsonValue.Bool(false); + + if (char == '[') return parse_array(); + if (char == '{') return parse_object(); + + fail(Strings.format1("expected JSON value, got character '%c'", char)); + return ERR_RET; + } + + def parse_string() -> JsonValue { + var res = Strings.parseLiteral(data, pos); + var len = res.0; + if (len <= 0) { + fail("invalid string"); + return ERR_RET; + } + return JsonValue.String(readToken(len).image); + } + + def parse_number() -> JsonValue { + var res = Ints.parseDecimal(data, pos); + var len = res.0, val = res.1; + if (len <= 0) { + fail("invalid number"); + } + advance(len); + return JsonValue.Number(val); + } + + private def parse_object_entry() -> (string, JsonValue) { + var ERR_VAL = ("", ERR_RET); + + var key = parse_string(); + if (!ok || req1(':') == -1) return ERR_VAL; + var val = parse_value(); + if (!ok) return ERR_VAL; + return (JsonValue.String.!(key).v, val); + } + + def parse_object() -> JsonValue { + var dict = HashMap.new(Strings.hash, Strings.equal); + var entry: (string, JsonValue); + if (req1('{') == -1) return ERR_RET; + entry = parse_object_entry(); + dict[entry.0] = entry.1; + if (!ok) return ERR_RET; + while (opt1(',') != -1) { + entry = parse_object_entry(); + if (!ok) return ERR_RET; + dict[entry.0] = entry.1; + } + if (req1('}') == -1) return ERR_RET; + return JsonValue.JObject(dict); + } + + def parse_array() -> JsonValue { + var vals = Vector.new(); + if (req1('[') == -1) return ERR_RET; + vals.put(parse_value()); + if (!ok) return ERR_RET; + while (opt1(',') != -1) { + vals.put(parse_value()); + if (!ok) return ERR_RET; + } + if (req1(']') == -1) return ERR_RET; + return JsonValue.JArray(vals.extract()); + } + + def test_eof() -> bool { + var eof = pos >= data.length; + if (eof) fail("unexpected end of input"); + return eof; + } +} + +class MapCollector { + def cmp: (K, K) -> bool; + def pairs = Vector<(K, V)>.new(); + + new(cmp, map: HashMap) { map.apply(collect); } + def collect(k: K, v: V) { pairs.put((k, v)); } + def extract() -> Array<(K, V)> { + var arr = pairs.extract(); + return Arrays.sort(arr, 0, arr.length, cmp_entries); + } + private def cmp_entries(a: (K, V), b: (K, V)) -> bool { + return cmp(a.0, b.0); + } +} diff --git a/test/lib/JsonTest.v3 b/test/lib/JsonTest.v3 new file mode 100644 index 000000000..e23cc75bf --- /dev/null +++ b/test/lib/JsonTest.v3 @@ -0,0 +1,59 @@ +def T = LibTests.register("Json", _, _); +def X = [ + T("render_literals", render_literals), + T("parse_literals", parse_literals), + () +]; + +def assert_parse_result(t: LibTest, src: string, expected: JsonValue) { + var p = JsonParser.new(src); + var res = p.parse_value(); + // if (!p.ok) { + // t.fail(Strings.format2("expected \"%q\", got error \"%s\"", expected.render, p.error_msg)); + // } + // if (!res.equal(expected)) { + // t.fail(Strings.format2("expected \"%q\", got \"%q\"", expected.render, res.render)); + // } +} + +def assert_render_result(t: LibTest, src: JsonValue, expected: string) { + var output = src.render(StringBuilder.new()).toString(); + if (!Strings.equal(output, expected)) { + t.fail(Strings.format2("expected \"%s\", got \"%s\"", expected, output)); + } +} + +def S = JsonValue.String; +def N = JsonValue.Number; +def B = JsonValue.Bool; +def Null = JsonValue.Null; +def A = JsonValue.JArray; +def O = JsonValue.JObject; +def NO_MAP = HashMap.new(Strings.hash, Strings.equal); + +def render_literals(t: LibTest) { + var test = assert_render_result(t, _, _); + + test(N(-16), "-16"); + test(N(0), "0"); + test(N(72), "72"); + test(N(65536), "65536"); + + test(S(""), "\"\""); + test(S("abc"), "\"abc\""); + test(S("abc\n"), "\"abc\\n\""); + test(S("abc\""), "\"abc\\\"\""); + + test(Null, "null"); + test(B(true), "true"); + test(B(false), "false"); + + test(A([]), "[]"); + test(A([A([A([A([A([])])])])]), "[[[[[]]]]]"); + test(O(NO_MAP), "{}"); + test(A([O(NO_MAP)]), "[{}]"); +} + +def parse_literals(t: LibTest) { + var test = assert_parse_result(t, _, _); +}