From db843d4538fd60fdad71c4481eace2bf00c37a34 Mon Sep 17 00:00:00 2001 From: Raphael Luba Date: Tue, 27 Jan 2026 15:49:55 +0100 Subject: [PATCH 1/2] json_write_native: Add default value for float_handling --- typed.jai | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typed.jai b/typed.jai index a50ee81..6e1f79a 100644 --- a/typed.jai +++ b/typed.jai @@ -36,7 +36,7 @@ json_parse_file :: (filename: string, $T: Type, ignore_unknown := true, rename : return success, result; } -json_write_native :: (builder: *String_Builder, data: *void, info: *Type_Info, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note, float_handling: Special_Float_Handling, level := 0) { +json_write_native :: (builder: *String_Builder, data: *void, info: *Type_Info, indent_char := "\t", ignore := ignore_by_note, rename := rename_by_note, float_handling := Special_Float_Handling.SUPPORT_NULL, level := 0) { if info.type == { case .BOOL; append(builder, ifx (.*)(cast(*bool) data) "true" else "false"); From 308d4dcf0b047740de07ea996a72df30df2d83d4 Mon Sep 17 00:00:00 2001 From: Stuart Mouse Date: Thu, 29 Jan 2026 17:38:01 -0600 Subject: [PATCH 2/2] implement type handlers for user extension --- handler.jai | 73 +++++++++++++++++++++++ module.jai | 1 + typed.jai | 163 ++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 214 insertions(+), 23 deletions(-) create mode 100644 handler.jai diff --git a/handler.jai b/handler.jai new file mode 100644 index 0000000..73662c5 --- /dev/null +++ b/handler.jai @@ -0,0 +1,73 @@ + +#add_context jaison: Jaison_Context; + +// TODO: since we are adding a context, we may as well use it for the rename proc and float handling... +Jaison_Context :: struct { + type_handlers: [..] JSON_Type_Handler; +} + +/* + I am not 100% satisfied with the design of handlers here, but this is the best I could come up with in light of the need for a split between preparing the slot and actually setting the value. + Maybe we don't really need that split and could just handle everything all at once, but that would require more extensive changes. + + Because of the split between preparing the slot and actually writing to it, we need to be able to attach some kind of user data in prepare_slot that we can read in get_member_slot or set_value_from_string. + And, since we are potentially way down in some recursive structure, our only option for allocating that user data is most likely to New() it. + So now that we have some user data that may (or may not!) be dynamically allocated, we need a means to free that later, hence the deinit proc. + + One plus to doing things this way is that we could override any of the handler procs on a per-instance basis, but how useful that really is is up for debate. +*/ + +JSON_Type_Handler :: struct { + type: *Type_Info; + user_data: *void; + + prepare_slot: (handler: *JSON_Type_Handler, expected_type: Type_Info_Tag, info: *Type_Info, slot: *void) -> success: bool, info: *Type_Info, slot: *void; + get_member_slot: (handler: *JSON_Type_Handler, info: *Type_Info, slot: *void, key: string, index: int) -> success: bool, info: *Type_Info, slot: *void; + set_value_from_string: (handler: *JSON_Type_Handler, info: *Type_Info, slot: *void, string_value: string) -> success: bool; + deinit: (handler: *JSON_Type_Handler); +} + +is_valid :: inline (handler: JSON_Type_Handler) -> bool { + return handler.type != null; +} + +deinit :: inline (handler: *JSON_Type_Handler) { + if is_valid(handler) { + if handler.deinit handler.deinit(handler); + handler.* = .{}; + } +} + +get_handler_for_type :: (info: *Type_Info) -> JSON_Type_Handler { + handlers := context.jaison.type_handlers; + if !handlers return .{}; + + // first iteration searches for specific type, repeating search if type is a variant + while true { + for handler: handlers { + if handler.type == info { + return handler; + } + } + if info.type == .VARIANT { + info = (cast(*Type_Info_Variant) info).variant_of; + continue; + } + break; + } + + // second iteration searches for match to polymorph_source_struct + if info.type == .STRUCT { + polymorph_source_struct := (cast(*Type_Info_Struct) info).polymorph_source_struct; + for handler: handlers { + if handler.type == xx polymorph_source_struct { + return handler; + } + } + } + + return .{}; +} + +// With a generic handler interface, you have options now as to whether you want to use a handler for Any_Hash_Table, or generate individual handlers for specific hash table types. +// You can also handle things like Fixed_Array and Fixed_String types, or even bucket arrays and the like. diff --git a/module.jai b/module.jai index 1102267..aed9b15 100644 --- a/module.jai +++ b/module.jai @@ -98,6 +98,7 @@ WHITESPACE_CHARS :: " \t\n\r"; #load "generic.jai"; #load "typed.jai"; +#load "handler.jai"; json_append_escaped :: (builder: *String_Builder, str: string) { remaining := str; diff --git a/typed.jai b/typed.jai index 6e1f79a..e0a69b3 100644 --- a/typed.jai +++ b/typed.jai @@ -177,8 +177,37 @@ parse_value :: (to_parse: string, slot: *u8, info: *Type_Info, ignore_unknown: b remainder := trim_left(to_parse, WHITESPACE_CHARS); if !remainder return "", false; success := true; - - prepare_slot :: (expected_type: Type_Info_Tag, info: *Type_Info, slot: *u8, to_parse: string) -> *u8, success: bool, is_generic: bool, info: *Type_Info { + + prepare_slot :: (expected_type: Type_Info_Tag, info: *Type_Info, slot: *u8, to_parse: string) -> *u8, success: bool, is_generic: bool, info: *Type_Info, handler: JSON_Type_Handler { + log_type_mismatch_error :: () #expand { + teaser := to_parse; + if teaser.count > 50 teaser.count = 50; + builder: String_Builder; + print_type_to_builder(*builder, info); + type_name := builder_to_string(*builder,, temp); + log_error("Cannot parse % value into type \"%\". Remaining input is: %…", expected_type, type_name, teaser); + } + + handler := get_handler_for_type(info); + if is_valid(handler) { + // To prevent extra complications with other primitive types, handlers are only permitted for JSON objects, arrays, and strings. + if expected_type == { + case .STRING; + case .STRUCT; + case .ARRAY; + case; + // TODO: better error message specific to handler case..? + log_type_mismatch_error(); + } + + success, value_info, value_slot := handler.prepare_slot(*handler, expected_type, info, slot); + if !success { + log_type_mismatch_error(); + return null, false, false, value_info, .{}; + } + return value_slot, true, false, value_info, handler; + } + value_info := info; if info.type == .POINTER { pointer_info := cast(*Type_Info_Pointer) info; @@ -193,13 +222,8 @@ parse_value :: (to_parse: string, slot: *u8, info: *Type_Info, ignore_unknown: b is_generic := is_generic_json_value(value_info); if !is_generic && value_info.type != expected_type { - teaser := to_parse; - if teaser.count > 50 teaser.count = 50; - builder: String_Builder; - print_type_to_builder(*builder, info); - type_name := builder_to_string(*builder,, temp); - log_error("Cannot parse % value into type \"%\". Remaining input is: %…", expected_type, type_name, teaser); - return null, false, false, value_info; + log_type_mismatch_error(); + return null, false, false, value_info, .{}; } if info.type == .POINTER { @@ -215,9 +239,12 @@ parse_value :: (to_parse: string, slot: *u8, info: *Type_Info, ignore_unknown: b memset(value_slot, 0, value_info.runtime_size); } (.*)cast(**u8)slot = value_slot; - return value_slot, true, is_generic, value_info; + + // needs to be called recursively in case we have a pointer to some type that requires a handler. + value_slot, success:, is_generic, value_info, handler = prepare_slot(expected_type, value_info, value_slot, to_parse); + return value_slot, success, is_generic, value_info, handler; } else { - return slot, true, is_generic, value_info; + return slot, true, is_generic, value_info, .{}; } } @@ -308,27 +335,36 @@ parse_value :: (to_parse: string, slot: *u8, info: *Type_Info, ignore_unknown: b log_error("Found unsupported string value \"%\" for field \"%\" of type %.", value, field_name, type_name); } } else { - value_slot: *u8; - value_slot, success, is_generic = prepare_slot(.STRING, info, slot, to_parse); + value_slot: *u8; + value_info: *Type_Info; + handler: JSON_Type_Handler; + defer deinit(*handler); + value_slot, success, is_generic, value_info, handler = prepare_slot(.STRING, info, slot, to_parse); if success { - if is_generic { + if is_valid(handler) { + success = handler.set_value_from_string(*handler, value_info, value_slot, value); + } else if is_generic { json_set(cast(*JSON_Value)value_slot, value); } else { (.*)cast(*string)value_slot = value; } - stored = true; + stored = success; } } } } case #char "["; - value_slot: *u8; - value_info: *Type_Info; + value_slot: *u8; + value_info: *Type_Info; + handler: JSON_Type_Handler; + defer deinit(*handler); if slot { - value_slot, success, is_generic, value_info = prepare_slot(.ARRAY, info, slot, to_parse); + value_slot, success, is_generic, value_info, handler = prepare_slot(.ARRAY, info, slot, to_parse); } if success { - if is_generic { + if is_valid(handler) { + remainder, success = parse_array_with_handler(*handler, remainder, value_slot, value_info, ignore_unknown, rename=rename, float_handling=float_handling); + } else if is_generic { value: [] JSON_Value; value, remainder, success = parse_array(remainder); json_set(cast(*JSON_Value)value_slot, value); @@ -337,13 +373,17 @@ parse_value :: (to_parse: string, slot: *u8, info: *Type_Info, ignore_unknown: b } } case #char "{"; - value_slot: *u8; - value_info: *Type_Info; + value_slot: *u8; + value_info: *Type_Info; + handler: JSON_Type_Handler; + defer deinit(*handler); if slot { - value_slot, success, is_generic, value_info = prepare_slot(.STRUCT, info, slot, to_parse); + value_slot, success, is_generic, value_info, handler = prepare_slot(.STRUCT, info, slot, to_parse); } if success { - if is_generic { + if is_valid(handler) { + remainder, success = parse_object_with_handler(*handler, remainder, value_slot, value_info, ignore_unknown, rename=rename, float_handling=float_handling); + } else if is_generic { value := New(JSON_Object); value.*, remainder, success = parse_object(remainder); json_set(cast(*JSON_Value)value_slot, value); @@ -462,6 +502,44 @@ parse_enum_string :: (str: string, slot: *u8, info_enum: *Type_Info_Enum) -> rem return remainder, success; } +parse_array_with_handler :: (handler: *JSON_Type_Handler, str: string, slot: *u8, info: *Type_Info, ignore_unknown: bool, rename: Rename_Proc, float_handling: Special_Float_Handling) -> remainder: string, success: bool { + assert(str[0] == #char "{", "Invalid object start %", str); + remainder := advance(str); + remainder = trim_left(remainder, WHITESPACE_CHARS); + if !remainder return "", false; + + if remainder[0] == #char "}" { + remainder = advance(remainder); + return remainder, true; + } + + index := 0; + while true { + defer index +=1; + + if !remainder || remainder[0] != #char "\"" return remainder, false; + + success, member_info, member_slot := handler.get_member_slot(handler, info, slot, "", index); + if !success return remainder, false; + + remainder = trim_left(remainder, WHITESPACE_CHARS); + if !remainder || remainder[0] != #char ":" return remainder, false; + remainder = advance(remainder); + + remainder, success = parse_value(remainder, member_slot, member_info, ignore_unknown, "", rename, float_handling=float_handling); + if !success return remainder, false; + + remainder = trim_left(remainder, WHITESPACE_CHARS); + if !remainder || remainder[0] != #char "," break; + remainder = advance(remainder); + remainder = trim_left(remainder, WHITESPACE_CHARS); + } + + if !remainder || remainder[0] != #char "}" return remainder, false; + remainder = advance(remainder); + return remainder, true; +} + parse_array :: (str: string, slot: *u8, info: *Type_Info_Array, ignore_unknown: bool, rename: Rename_Proc, float_handling: Special_Float_Handling) -> remainder: string, success: bool { element_size: int; if slot { @@ -599,6 +677,45 @@ fill_member_table :: (table: *Table(string, Member_Offset), info: *Type_Info_Str } } +parse_object_with_handler :: (handler: *JSON_Type_Handler, str: string, slot: *u8, info: *Type_Info, ignore_unknown: bool, rename: Rename_Proc, float_handling: Special_Float_Handling) -> remainder: string, success: bool { + assert(str[0] == #char "{", "Invalid object start %", str); + remainder := advance(str); + remainder = trim_left(remainder, WHITESPACE_CHARS); + if !remainder return "", false; + + if remainder[0] == #char "}" { + remainder = advance(remainder); + return remainder, true; + } + + index := 0; + while true { + defer index +=1; + + if !remainder || remainder[0] != #char "\"" return remainder, false; + + key, remainder=, success := parse_string(remainder,, temp); + if !success return remainder, false; + + success=, member_info, member_slot := handler.get_member_slot(handler, info, slot, key, index); + + remainder = trim_left(remainder, WHITESPACE_CHARS); + if !remainder || remainder[0] != #char ":" return remainder, false; + remainder = advance(remainder); + + remainder, success = parse_value(remainder, member_slot, member_info, ignore_unknown, key, rename, float_handling=float_handling); + if !success return remainder, false; + + remainder = trim_left(remainder, WHITESPACE_CHARS); + if !remainder || remainder[0] != #char "," break; + remainder = advance(remainder); + remainder = trim_left(remainder, WHITESPACE_CHARS); + } + + if !remainder || remainder[0] != #char "}" return remainder, false; + remainder = advance(remainder); + return remainder, true; +} parse_object :: (str: string, slot: *u8, info: *Type_Info_Struct, ignore_unknown: bool, rename: Rename_Proc, float_handling: Special_Float_Handling) -> remainder: string, success: bool { assert(str[0] == #char "{", "Invalid object start %", str);