// This file contains just the JSON serialization functions. See generic.jai and typed.jai for parse fuctions. // Generates a JSON string from either a JSON_Value or any custom type. // "indent_char" does what it says on the tin. // "ignore" is only used for custom types to determine which properties of your custom type should be serialized. // The default ignore function ignores all struct members that have the note @JsonIgnore. // "rename" is used for renaming certain members. // It gets called with the Type_Info_Struct_Member and must return the new name of the field. // The default procedure rename members by their @JsonName note. Eg: @JsonName(renamed_member). json_write_string :: (value: $T, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note, float_handling := Special_Float_Handling.SUPPORT_NULL) -> string { builder: String_Builder; defer free_buffers(*builder); json_append_value(*builder, value, indent_char, ignore, rename, float_handling); return builder_to_string(*builder); } json_write_file :: (filename: string, value: $T, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note, float_handling := Special_Float_Handling.SUPPORT_NULL) -> bool { builder: String_Builder; defer free_buffers(*builder); json_append_value(*builder, value, indent_char, ignore, rename, float_handling); return write_entire_file(filename, *builder); } json_append_value :: (builder: *String_Builder, val: $T, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note, float_handling := Special_Float_Handling.SUPPORT_NULL) { #if T == JSON_Value { json_write_json_value(builder, val, indent_char, float_handling=float_handling); } else { info := type_info(T); json_write_native(builder, *val, info, indent_char, ignore, rename, float_handling=float_handling); } } // This function is useful if you have a JSON template string and just want to // safely insert a value without having to replicate the full json structure in Jai. // The return value does NOT include quotes around the string. // // Example: // JSON_TEMPLATE :: #string END // { // "complicated": { // "json": { // "structure": { // "for_a_stupid_api": { // "that_needs": [ // {"a_deeply_nested_value": "%1"} // ] // } // } // } // } // } // END // escaped_value := json_escape_string(my_unsafe_value); // defer free(escaped_value); // json_str := print(JSON_TEMPLATE, escaped_value); json_escape_string :: (str: string) -> string { builder: String_Builder; defer free_buffers(*builder); json_append_escaped(*builder, str); escaped := builder_to_string(*builder); return escaped; } // How shall NaN and Infinity be serialized / deserialized? Special_Float_Handling :: enum_flags { SUPPORT_NULL :: 0x01; SUPPORT_STRINGS :: 0x02; SUPPORT_ALL :: SUPPORT_NULL | SUPPORT_STRINGS; } Ignore_Proc :: #type (member: *Type_Info_Struct_Member) -> bool; Rename_Proc :: #type (member: *Type_Info_Struct_Member) -> string; ignore_by_note :: (member: *Type_Info_Struct_Member) -> bool { for note: member.notes { if note == "JsonIgnore" return true; } return false; } rename_by_note :: (member: *Type_Info_Struct_Member) -> string { for note: member.notes { if !begins_with(note, "JsonName(") continue; if note.count <= 10 || note[note.count-1] != #char ")" { log_error("Invalid JsonName note format. Expected a name in parenthesis, but the note was \"%\".", note); continue; } return slice(note, 9, note.count-10); } return member.name; } #scope_module WHITESPACE_CHARS :: " \t\n\r"; #load "generic.jai"; #load "typed.jai"; json_append_escaped :: (builder: *String_Builder, str: string) { remaining := str; next_pos := index_of_illegal_string_char(remaining); append(builder, "\""); while (next_pos >= 0) { append(builder, slice(remaining, 0, next_pos)); if remaining[next_pos] == { case #char "\\"; append(builder, "\\\\"); case #char "\""; append(builder, "\\\""); case #char "\n"; append(builder, "\\n"); case #char "\r"; append(builder, "\\r"); case #char "\t"; append(builder, "\\t"); case; // ToDo: handle illegal multi-byte characters // print("Escaping: %\n\n", slice(remaining, next_pos, remaining.count - next_pos)); print_to_builder(builder, "\\u%", formatInt(remaining[next_pos], base=16, minimum_digits=4)); } remaining = advance(remaining, next_pos + 1); next_pos = index_of_illegal_string_char(remaining); } append(builder, remaining); append(builder, "\""); } index_of_illegal_string_char :: (str: string) -> s64 { for 0..str.count - 1 { if str[it] == #char "\\" || str[it] == #char "\"" || str[it] <= 0x1F { return it; } } return -1; } expect_and_slice :: (str: string, expected: string) -> remainder: string, success: bool { if str.count < expected.count || !equal(slice(str, 0, expected.count), expected) { log_error("Unexpected token. Expected \"%\" but got: %", expected, str); return str, false; } remainder := advance(str, expected.count); return remainder, true; } parse_string :: (str: string) -> result: string, remainder: string, success: bool { assert(str[0] == #char "\"", "Invalid string start %", str); inside := advance(str); if !inside return "", "", false; needsUnescape := false; while true { if !inside || (inside.count < 2 && inside[0] != #char "\"") return "", str, false; if inside[0] == #char "\"" break; if inside[0] == #char "\\" { needsUnescape = true; if inside.count < 2 return "", str, false; advance(*inside); } advance(*inside); } length := inside.data - str.data - 1; result := slice(str, 1, length); if needsUnescape { success: bool; result, success = unescape(result); if !success return "", str, false; } else { result = copy_string(result); } remainder := slice(str, length + 2, str.count - length - 2); return result, remainder, true; } unescape :: (str: string) -> result: string, success: bool { result := alloc_string(str.count); rc := 0; success := true; for i: 0..str.count-1 { if str[i] != #char "\\" { // Check for invalid characters for JSON if str[i] < 0x20 { success = false; break; } result[rc] = str[i]; rc += 1; } else { if i == str.count - 1 { success = false; break; } i += 1; if str[i] == { case #char "\""; #through; case #char "/"; #through; case #char "\\"; result[rc] = str[i]; rc += 1; case #char "b"; result[rc] = 0x08; rc += 1; case #char "f"; result[rc] = 0x0c; rc += 1; case #char "n"; result[rc] = #char "\n"; rc += 1; case #char "r"; result[rc] = #char "\r"; rc += 1; case #char "t"; result[rc] = #char "\t"; rc += 1; case #char "u"; if i + 4 >= str.count { success = false; break; } unicode_char, unicode_success := parse_unicode(slice(str, i + 1, 4)); if !unicode_success { success = false; break; } // A high surrogate followed by \u and a low one is a single character beyond // U+FFFF, such as an emoji: four bytes of UTF-8, not two 3-byte halves (which are // not valid UTF-8). A surrogate without its other half is encoded on its own, as before. if unicode_char >= 0xD800 && unicode_char <= 0xDBFF && i + 10 < str.count && str[i + 5] == #char "\\" && str[i + 6] == #char "u" { low, low_success := parse_unicode(slice(str, i + 7, 4)); if low_success && low >= 0xDC00 && low <= 0xDFFF { code_point := 0x10000 + ((cast(u32) unicode_char - 0xD800) << 10) + (cast(u32) low - 0xDC00); result[rc] = xx (0xF0 | (code_point >> 18)); result[rc + 1] = xx (0x80 | ((code_point >> 12) & 0x3F)); result[rc + 2] = xx (0x80 | ((code_point >> 6) & 0x3F)); result[rc + 3] = xx (0x80 | (code_point & 0x3F)); rc += 4; i += 10; continue; } } utf8_len := encode_utf8(unicode_char, *(result.data[rc])); rc += utf8_len; i += 4; case; success = false; break; } } } if !success { free(result); return "", false; } result.count = rc; return result, true; } append_special_float :: (builder: *String_Builder, float_handling: Special_Float_Handling, is_nan: bool, is_negative: bool) { if !(float_handling & .SUPPORT_STRINGS) { append(builder, "null"); } else if is_nan { append(builder, "\"NaN\""); } else if is_negative { append(builder, "\"-Infinity\""); } else { append(builder, "\"Infinity\""); } } get_nan_or_infinity :: (value: u32) -> is_nan: bool, is_infinity: bool, is_negative: bool { is_special := (value & 0x7F80_0000) == 0x7F80_0000; mant := (value & 0x007F_FFFF); return (is_special && mant), is_special && !mant, !!(value & 0x8000_0000); } get_nan_or_infinity :: (value: u64) -> is_nan: bool, is_infinity: bool, is_negative: bool { is_special := (value & 0x7FF0_0000_0000_0000) == 0x7FF0_0000_0000_0000; mant := (value & 0xF_FFFF_FFFF_FFF); return (is_special && mant), is_special && !mant, !!(value & 0x8000_0000_0000_0000); } NAN :: 0h7FFF_FFFF_FFFF_FFFF; INFINITY :: 0h7FF0_0000_0000_0000; NEGATIVE_INFINITY :: 0h8FF0_0000_0000_0000; #import "Basic"; #import "File"; #import "String"; #import "Hash_Table"; #import,dir "./unicode_utils"; #import "IntroSort";