jc/encoding/json.jai

612 lines
17 KiB
Text

#module_parameters(RUN_TESTS := false);
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_serialize :: (value: $T) -> Json_Value {
json_serialize_with_info :: (info: *Type_Info, value: *void) -> Json_Value {
if value == null {
return .{ kind = .none };
}
if info.type == {
case .BOOL;
return .{ kind = .bool, b = value.(*bool).* };
case .INTEGER;
i_info := info.(*Type_Info_Integer);
if i_info.signed {
if i_info.runtime_size == {
case 1; return .{ kind = .number, n = (value.( *s8).*).(float64) };
case 2; return .{ kind = .number, n = (value.(*s16).*).(float64) };
case 4; return .{ kind = .number, n = (value.(*s32).*).(float64) };
case 8; return .{ kind = .number, n = (value.(*s64).*).(float64) };
case; basic.assert(false, "unknown float size: %", info.runtime_size);
}
}
else {
if i_info.runtime_size == {
case 1; return .{ kind = .number, n = (value.( *u8).*).(float64) };
case 2; return .{ kind = .number, n = (value.(*u16).*).(float64) };
case 4; return .{ kind = .number, n = (value.(*u32).*).(float64) };
case 8; return .{ kind = .number, n = (value.(*u64).*).(float64) };
case; basic.assert(false, "unknown float size: %", info.runtime_size);
}
}
case .FLOAT;
if info.runtime_size == {
case 4; return .{ kind = .number, n = (value.(*float32).*).(float64) };
case 8; return .{ kind = .number, n = (value.(*float64).*) };
case; basic.assert(false, "unknown float size: %", info.runtime_size);
}
case .STRING;
return .{ kind = .string, s = (value.(*string)).* };
case .STRUCT;
json_obj: Json_Value;
json_obj.kind = .object;
obj := *json_obj.object;
vptr := value.(*u8);
s_info := info.(*Type_Info_Struct);
for s_info.members if it.offset_in_bytes >= 0 {
name := get_json_member_name(it);
basic.array_add(*obj.keys, name);
field_value := vptr + it.offset_in_bytes;
basic.array_add(*obj.values, json_serialize_with_info(it.type, field_value));
}
return json_obj;
case .ARRAY;
json_array: Json_Value;
json_array.kind = .array;
arr := *json_array.array;
vptr := value.(*u8);
a_info := info.(*Type_Info_Array);
el_info := a_info.element_type;
if a_info.array_type == {
case .FIXED;
for 0..a_info.array_count - 1 {
el_ptr := vptr + (it * el_info.runtime_size);
basic.array_add(arr, json_serialize_with_info(el_info, el_ptr));
}
case .VIEW; #through;
case .RESIZABLE;
data := vptr.(*[]u8);
for 0..data.count - 1 {
el_ptr := data.data + (it * el_info.runtime_size);
basic.array_add(arr, json_serialize_with_info(el_info, el_ptr));
}
}
return json_array;
}
basic.assert(false, "unhandled type in marshal: %", info.type);
return .{};
}
return json_serialize_with_info(T.(*Type_Info), *value);
}
json_deserialize :: (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_deserialize :: ($T: Type, data: string) -> (T, bool) {
json_deserialize_with_info :: (info: *Type_Info, json: Json_Value, ptr: *void) -> bool {
if json.kind == {
case .none; return true;
case .bool;
basic.assert(info.type == .BOOL, "cannot deserialize % into %", json.b, info.type); // @errors
ptr.(*bool).* = json.b;
case .number;
if info.type == {
case .INTEGER;
i_info := info.(*Type_Info_Integer);
if i_info.signed {
// @todo(judah): bounds check to make sure this is safe
if i_info.runtime_size == {
case 1; ptr.(*s8 ).* = json.n.(s8 );
case 2; ptr.(*s16).* = json.n.(s16);
case 4; ptr.(*s32).* = json.n.(s32);
case 8; ptr.(*s64).* = json.n.(s64);
case; basic.assert(false, "unhandled int size % in deserialize", i_info.runtime_size);
}
}
else {
if i_info.runtime_size == {
case 1; ptr.(*u8 ).* = json.n.(u8 );
case 2; ptr.(*u16).* = json.n.(u16);
case 4; ptr.(*u32).* = json.n.(u32);
case 8; ptr.(*u64).* = json.n.(u64);
case; basic.assert(false, "unhandled uint size % in deserialize", i_info.runtime_size);
}
}
case .FLOAT;
if info.runtime_size == {
case 4; ptr.(*float32).* = json.n.(float32);
case 8; ptr.(*float64).* = json.n.(float64);
case; basic.assert(false, "unhandled float size % in deserialize", info.runtime_size);
}
}
return true;
case .string;
basic.assert(info.type == .STRING, "cannot deserialize '%' into %", json.s, info.type); // @errors
sptr := ptr.(*string);
sptr.data = basic.copy_string(json.s).data;
sptr.count = json.s.count;
return true;
case .object;
basic.assert(info.type == .STRUCT, "cannot deserialize json object into %", info.type); // @errors
vptr := ptr.(*u8);
s_info := info.(*Type_Info_Struct);
for mem: s_info.members {
found := false;
name := get_json_member_name(mem);
value: Json_Value;
for json.object.keys if it == name { // @todo(judah): handle custom names in notes
found = true;
value = json.object.values[it_index];
break;
}
if !found continue;
field_ptr := vptr + mem.offset_in_bytes;
if !json_deserialize_with_info(mem.type, value, field_ptr) {
return false;
}
}
return true;
case .array;
basic.assert(info.type == .ARRAY, "cannot deserialize json array into %", info.type); // @errors
vptr := ptr.(*u8);
a_info := info.(*Type_Info_Array);
el_info := a_info.element_type;
if a_info.array_type == {
case .FIXED;
basic.assert(json.array.count <= a_info.array_count);
for 0..a_info.array_count - 1 {
el_ptr := vptr + (it * el_info.runtime_size);
value := json.array[it];
if !json_deserialize_with_info(el_info, value, el_ptr) {
return false;
}
}
case .VIEW; #through;
case .RESIZABLE;
data := basic.alloc(json.array.count * el_info.runtime_size).(*u8);
if json.array.count == 0 {
return true;
}
// @todo(judah): verify the json array is homogenous
for json.array {
el_ptr := data + (it_index * el_info.runtime_size);
if !json_deserialize_with_info(el_info, it, el_ptr) {
return false;
}
}
if a_info.array_type == .VIEW {
out := ptr.(*[]u8);
out.data = data;
out.count = json.array.count;
}
else {
out := ptr.(*[..]u8);
out.data = data;
out.count = json.array.count;
out.allocated = json.array.count;
}
return true;
}
return true;
}
basic.assert(false, "unhandled json value: %", json.kind);
return false;
}
json, ok := json_deserialize(data);
if !ok return zero_of(T), ok;
out: T;
ok = json_deserialize_with_info(T.(*Type_Info), json, *out);
return out, ok;
}
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;
get_json_member_name :: (member: Type_Info_Struct_Member) -> string {
name := member.name;
for member.notes if strings.contains(it, "json:") {
name = .{ data = it.data + 5, count = it.count - 5 };
name = strings.replace(name, "\\_", " ");
break;
}
return name;
}
Parser :: struct {
allocator: Allocator;
data: []u8;
error: string;
offset: u64;
line: u64;
}
Token :: struct {
kind: enum {
invalid;
eof;
true_;
false_;
null_;
number;
str;
colon :: #char ":";
comma :: #char ",";
l_brace :: #char "{";
r_brace :: #char "}";
l_square :: #char "[";
r_square :: #char "]";
};
str: string;
}
get_next_token :: (p: *Parser) -> Token {
skip_whitespace(p);
if at_end(p) {
return .{ kind = .eof };
}
c := p.data[p.offset];
if c == {
case #char ":"; #through;
case #char ","; #through;
case #char "{"; #through;
case #char "}"; #through;
case #char "["; #through;
case #char "]";
p.offset += 1;
return .{ kind = xx c };
}
// Comments
if c == #char "/" && p.offset + 1 < p.data.count.(u64) && p.data[p.offset+1] == #char "/" {
capture_while(p, c => c != #char "\n");
return get_next_token(p);
}
// Strings
// @todo(judah): handle escaped quotes
if c == #char "\"" {
p.offset += 1; // skip "
str := capture_while(p, s => s != #char "\"");
p.offset += 1; // skip "
return .{ kind = .str, str = str };
}
// Numbers
if (c >= #char "0" && c <= #char "9") || c == #char "." {
number := capture_while(p, n => (n >= #char "0" && n <= #char "9") || n == #char ".");
return .{ kind = .number, str = number };
}
// true, false, null
if (c >= #char "a" && c <= #char "z") {
keyword := capture_while(p, n => (n >= #char "a" && n <= #char "z"));
if keyword == {
case "true";
return .{ kind = .true_ };
case "false";
return .{ kind = .false_ };
case "null";
return .{ kind = .null_ };
case;
return .{ kind = .invalid, str = keyword };
}
}
return .{ kind = .eof };
}
peek_token :: (p: *Parser) -> Token {
copy := p.*; // @todo(judah): this is bad?
return get_next_token(*copy);
}
expect :: (p: *Parser, kinds: ..type_of(Token.kind)) -> (bool, Token) {
tok := get_next_token(p);
for kinds if it == tok.kind {
return true, tok;
}
return false, tok;
}
parse_value :: (p: *Parser) -> Json_Value {
to_string :: (c: u8) -> string #expand {
str: string;
str.data = *c;
str.count = 1;
return str;
}
ok, tok := expect(p, .l_brace, .l_square, .str, .number, .true_, .false_, .null_);
if !ok {
set_error(p, "expected value, found '%'", tok.str);
return .{};
}
if tok.kind == {
case .number;
number := tok.str;
// @todo(judah): parse_float works a bit weird
f, ok := strings.parse_float(*number);
if !ok {
set_error(p, "malformed number '%'", tok.str);
return .{};
}
return .{ kind = .number, n = f };
case .true_; #through;
case .false_;
return .{ kind = .bool, b = tok.kind == .true_ };
case .null_;
return .{ kind = .none };
case .str;
return .{ kind = .string, s = tok.str };
case .l_brace;
object: Json_Value;
object.kind = .object;
obj := *object.object;
obj.keys.allocator = p.allocator;
obj.values.allocator = p.allocator;
while !at_end(p) {
end := peek_token(p);
if end.kind == .r_brace break;
key_ok, key := expect(p, .str);
if !key_ok {
set_error(p, "expected key for object");
return .{};
}
if !expect(p, .colon) {
set_error(p, "missing ':' after key");
return .{};
}
value := parse_value(p);
if p.error.count != 0 {
return .{};
}
basic.array_add(*obj.keys, key.str);
basic.array_add(*obj.values, value);
end = peek_token(p);
if end.kind == .r_brace break;
if !expect(p, .comma) {
set_error(p, "missing ',' after value");
return .{};
}
}
if !expect(p, .r_brace) {
set_error(p, "missing '}' at end of object");
return .{};
}
return object;
case .l_square;
array: Json_Value;
array.kind = .array;
array.array.allocator = p.allocator;
while !at_end(p) {
end := peek_token(p);
if end.kind == .r_square break;
value := parse_value(p);
if p.error.count != 0 {
return .{};
}
basic.array_add(*array.array, value);
end = peek_token(p);
if end.kind == .r_square break;
if !expect(p, .comma) {
set_error(p, "missing ',' after value");
return .{};
}
}
if !expect(p, .r_square) {
set_error(p, "missing ']' at end of array");
return .{};
}
return array;
}
return .{};
}
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.(u64));
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).(s64);
return capture.(string);
}
at_end :: inline (p: *Parser) -> bool {
return p.offset >= p.data.count.(u64);
}
basic :: #import "Basic"; // @future
strings :: #import "String"; // @future