From 325d8e2e81b46fb49f4be6947ab9c256be8300c5 Mon Sep 17 00:00:00 2001 From: Judah Caruso Date: Mon, 12 May 2025 22:03:57 -0600 Subject: [PATCH] encoding/json start --- encoding/json.jai | 309 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 encoding/json.jai diff --git a/encoding/json.jai b/encoding/json.jai new file mode 100644 index 0000000..de571fa --- /dev/null +++ b/encoding/json.jai @@ -0,0 +1,309 @@ +Json_Value :: struct { + kind: enum { + none; + bool; + number; + string; + array; + object; + }; + + using value: union { + b: bool; + n: float64; + s: string; + + array: [..]Json_Value; + + object: struct { + keys: [..]string; + values: [..]Json_Value; + } + } +} + +json_marshal :: (value: $T) -> string { + return ""; +} + +json_unmarshal :: (data: string) -> (Json_Value, bool) { + p := Parser.{ + data = data.([]u8), + allocator = context.allocator, + }; + + // @todo(judah): logging + return parse_value(*p), p.error.count == 0; +} + +json_unmarshal :: (data: string, $T: Type) -> (T, bool) { + zero: T; + return zero, false; +} + +to_string :: (value: Json_Value, indent := 0) -> string { + b: basic.String_Builder; + + add_indents :: (b: *basic.String_Builder, level: int) #expand { + if level != 0 { + for 0..level basic.append(b, " "); + } + } + + if value.kind == { + case .none; + basic.append(*b, "null"); + + case .bool; + if value.b { + basic.append(*b, "true"); + } + else { + basic.append(*b, "false"); + } + + case .number; + basic.print_to_builder(*b, "%", value.n); + + case .string; + basic.print_to_builder(*b, "\"%\"", value.s); + + case .array; + basic.append(*b, "[\n"); + + for value.array { + add_indents(*b, indent + 2); + basic.append(*b, to_string(it, indent + 2)); + if it_index < value.array.count { + basic.append(*b, ",\n"); + } + } + + add_indents(*b, indent); + basic.append(*b, "]"); + + case .object; + basic.append(*b, "{\n"); + + for value.object.keys { + add_indents(*b, indent + 2); + v := value.object.values[it_index]; + basic.print_to_builder(*b, "\"%\": %,\n", it, to_string(v, indent + 2)); + } + + add_indents(*b, indent); + basic.append(*b, "}"); + } + + return basic.builder_to_string(*b); +} + + +#scope_file; + +Parser :: struct { + allocator: Allocator; + data: []u8; + error: string; + offset: uint; + line: uint; +} + +parse_value :: (p: *Parser) -> Json_Value { + skip_whitespace(p); + + if at_end(p) { + return .{}; + } + + to_string :: (c: u8) -> string #expand { + str: string; + str.data = *c; + str.count = 1; + return str; + } + + c := p.data[p.offset]; + if c == { + case #char "{"; + p.offset += 1; // skip { + + object: Json_Value; + object.kind = .object; + object.object.keys.allocator = p.allocator; + object.object.values.allocator = p.allocator; + + skip_whitespace(p); + while !at_end(p) { + c = p.data[p.offset]; + if c == #char "}" break; + + key := parse_value(p); + if p.error.count != 0 || key.kind != .string { + set_error(p, "expected key for object"); + return .{}; + } + + if !expect(p, #char ":") { + set_error(p, "missing ':' after key"); + return .{}; + } + + value := parse_value(p); + if p.error.count != 0 { + return .{}; + } + + if !expect(p, #char ",") { + set_error(p, "expected ',' after value in object"); + return .{}; + } + + basic.array_add(*object.object.keys, key.s); + basic.array_add(*object.object.values, value); + } + + p.offset += 1; // skip } + return object; + + case #char "["; + p.offset += 1; // skip [ + + array: Json_Value; + array.kind = .array; + array.array.allocator = p.allocator; + + skip_whitespace(p); + while !at_end(p) { + c = p.data[p.offset]; + if c == #char "]" break; + + v := parse_value(p); + if p.error.count != 0 { + set_error(p, "expected value for array"); + return .{}; + } + + if !expect(p, #char ",") { + set_error(p, "expected ',' after value in array"); + return .{}; + } + + basic.array_add(*array.array, v); + } + + p.offset += 1; // skip ] + + return array; + + case #char "\""; + p.offset += 1; + + str := capture_while(p, s => s != #char "\""); + basic.assert(str.count != 0); + + p.offset += 1; + + return .{ kind = .string, s = str }; + + case; + // number + if (c >= #char "0" && c <= #char "9") || c == #char "." { + number := capture_while(p, n => (n >= #char "0" && n <= #char "9") || n == #char "."); + basic.assert(number.count != 0); + + // @todo(judah): parse_float works a bit weird + f, ok := strings.parse_float(*number); + if !ok { + set_error(p, "malformed number '%'", number); + return .{}; + } + + return .{ kind = .number, n = f }; + } + + // true, false, null + if (c >= #char "a" && c <= #char "z") { + keyword := capture_while(p, k => k >= #char "a" && k <= #char "z"); + basic.assert(keyword.count != 0); + + if keyword == { + case "true"; + return .{ kind = .bool, b = true }; + case "false"; + return .{ kind = .bool, b = false }; + case "null"; + return .{ kind = .none }; + } + + set_error(p, "expected number, true, false, null but found '%'", keyword); + return .{}; + } + + set_error(p, "expected number, true, false, null but found '%'", to_string(c)); + return .{}; + } + + return .{}; +} + +expect :: (p: *Parser, c: u8) -> bool { + if at_end(p) || p.data[p.offset] != c { + return false; + } + + p.offset += 1; + skip_whitespace(p); + return true; +} + +set_error :: (p: *Parser, message: string, args: ..Any) { + p.error = basic.tprint("line %, %", p.line, basic.tprint(message, ..args)); +} + +skip_whitespace :: (p: *Parser) { + while !at_end(p) { + c := p.data[p.offset]; + if c == { + case #char " "; #through; + case #char "\t"; #through; + case #char "\r"; + p.offset += 1; + + case #char "\n"; + p.offset += 1; + p.line += 1; + + case; + break; + } + } +} + +capture_while :: (p: *Parser, proc: (u8) -> bool) -> string { + basic.assert(p.offset < p.data.count.(uint)); + + capture := p.data; + capture.data += p.offset; + capture.count = 0; + + while !at_end(p) { + c := p.data[p.offset]; + if !proc(c) break; + p.offset += 1; + } + + capture.count = ((p.data.data + p.offset) - capture.data).(sint); + return capture.(string); +} + +at_end :: inline (p: *Parser) -> bool { + return p.offset >= p.data.count.(uint); +} + +basic :: #import "Basic"; +strings :: #import "String"; + +#import,file "../base.jai"; + +