package store // Ini Reflection code import "base:runtime" import "core:mem" import "core:fmt" import "core:time" import "core:strconv" import "core:strings" import "core:reflect" import "core:encoding/ini" // import "core:encoding/json" Ini_Missing_Category :: distinct string Ini_Missing_Field :: distinct string Ini_Unmarshal_Error :: union { mem.Allocator_Error, Ini_Missing_Category, Ini_Missing_Field, } /* Get the value for a subtag. This is useful if you need to parse through the `args` tag for a struct field on a custom type setter or custom flag checker. Example: import "core:flags" import "core:fmt" subtag_example :: proc() { args_tag := "precision=3,signed" precision, has_precision := flags.get_subtag(args_tag, "precision") signed, is_signed := flags.get_subtag(args_tag, "signed") fmt.printfln("precision = %q, %t", precision, has_precision) fmt.printfln("signed = %q, %t", signed, is_signed) } Output: precision = "3", true signed = "", true */ ini_get_subtag :: proc(tag, id: string) -> (value: string, ok: bool) { // This proc was initially private in `internal_rtti.odin`, but given how // useful it would be to custom type setters and flag checkers, it lives // here now. tag := tag for subtag in strings.split_iterator(&tag, ",") { if equals := strings.index_byte(subtag, '='); equals != -1 && id == subtag[:equals] { return subtag[1 + equals:], true } else if id == subtag { return "", true } } return } ini_get_subtag_with_type :: proc(tag, subtag: string, $T: typeid) -> (t: T, ok: bool) { ok = ini_parse_and_set_pointer_by_base_type(&t, ini_get_subtag(tag, subtag) or_return, type_info_of(T)) return } @private ini_rtti_get_struct :: proc(type: ^runtime.Type_Info, named: string = "") -> (base_type: ^runtime.Type_Info, base_struct_type: runtime.Type_Info_Struct, name: string, ok: bool) { name = named #partial switch t in type.variant { case runtime.Type_Info_Struct: base_type = type base_struct_type = t ok = true case runtime.Type_Info_Named: name = t.name return ini_rtti_get_struct(t.base, name) case: ok = false } return } ini_unmarshal :: proc(data: string, v: ^$T, allocator := context.allocator) -> Ini_Unmarshal_Error { m := ini.load_map_from_string(data, allocator) or_return defer ini.delete_map(m) fields := reflect.struct_fields_zipped(T) for field in fields { base_type, _, _, t_ok := ini_rtti_get_struct(field.type) if !t_ok { fmt.panicf( "INI: Invalid type for field %q on %T, expected a struct but got %v", field.name, v^, field, ) } tag, _ := reflect.struct_tag_lookup(field.tag, "ini") name := ini_get_subtag(tag, "name") or_else field.name _, required := ini_get_subtag(tag, "required") category, ok := m[name] if !ok { if required { return Ini_Missing_Category(strings.clone(name, allocator = allocator)) } } struct_fields := reflect.struct_fields_zipped(base_type.id) for struct_field in struct_fields { struct_field_tag, _ := reflect.struct_tag_lookup(struct_field.tag, "ini") _, struct_field_required := ini_get_subtag(struct_field_tag, "required") struct_field_name := ini_get_subtag(struct_field_tag, "name") or_else struct_field.name default: Maybe(string) default_ok: bool default, default_ok = ini_get_subtag_with_type(struct_field_tag, "default", string) defer if default != nil { delete(default.?) } if !default_ok { default = nil } value: string if value, ok = category[struct_field_name]; !ok { if value, ok = default.?; !ok && struct_field_required { return Ini_Missing_Field(strings.clone(struct_field_name, allocator = allocator)) } } ini_parse_and_set_pointer_by_base_type(cast(rawptr)(uintptr(v)+field.offset+struct_field.offset), value, struct_field.type) } } return nil } ini_parse_and_set_pointer_by_base_type :: proc(ptr: rawptr, str: string, type_info: ^runtime.Type_Info, allocator := context.allocator) -> bool { #partial switch type_info_variant in type_info.variant { case runtime.Type_Info_Boolean: value := strconv.parse_bool(str) or_return switch type_info.id { case bool: (cast(^bool)ptr)^ = value case b8: (cast(^b8) ptr)^ = b8(value) case b16: (cast(^b16) ptr)^ = b16(value) case b32: (cast(^b32) ptr)^ = b32(value) case b64: (cast(^b64) ptr)^ = b64(value) } case runtime.Type_Info_String: if type_info_variant.is_cstring { cstr_ptr := cast(^cstring)ptr if cstr_ptr != nil { delete(cstr_ptr^) } cstr_ptr^ = strings.clone_to_cstring(str, allocator = allocator) } else { (cast(^string)ptr)^ = strings.clone(str, allocator = allocator) } case: // Others if type_info.id == time.Duration { l: int value, ok := strconv.parse_i64_of_base(str, 10, &l) if !ok && l <= 0 { return false } else if len(str) > l { ending := str[l] switch ending { case 'h': (cast(^time.Duration)ptr)^ = time.Duration(value) * time.Duration( time.Hour) case 'm': (cast(^time.Duration)ptr)^ = time.Duration(value) * time.Duration(time.Minute) case 's': (cast(^time.Duration)ptr)^ = time.Duration(value) * time.Duration(time.Second) case: return false } } else if ok { (cast(^time.Duration)ptr)^ = time.Duration(value) return true } else { return false } } else { return false } } return true } ini_bool_default :: proc(field: reflect.Struct_Field, default: string, default_found: string) { } import te "core:testing" @test ini_parse_and_set_pointer_by_base_type_success :: proc(t: ^te.T) { test_with_type_info :: proc(t: ^te.T, $T: typeid, str: string, expected: T) { val: T te.expect_value(t, ini_parse_and_set_pointer_by_base_type(&val, str, type_info_of(T)), true) te.expectf(t, val == expected, "expected value of type %T to be %v, but got %v", expected, expected, val) } test_with_type_info(t, bool, "true", true) test_with_type_info(t, b8, "true", true) test_with_type_info(t, b16, "true", true) test_with_type_info(t, b32, "true", true) test_with_type_info(t, b64, "true", true) test_with_type_info(t, bool, "false", false) test_with_type_info(t, time.Duration, "10", 10) test_with_type_info(t, time.Duration, "10h", time.Duration(10 * time.Hour)) test_with_type_info(t, time.Duration, "10m", time.Duration(10 * time.Minute)) test_with_type_info(t, time.Duration, "10s", time.Duration(10 * time.Second)) } @test ini_parse_and_set_pointer_by_base_type_fail :: proc(t: ^te.T) { test_with_type_info :: proc(t: ^te.T, $T: typeid, str: string) { val: T te.expect_value(t, ini_parse_and_set_pointer_by_base_type(&val, str, type_info_of(T)), false) } test_with_type_info(t, bool, "deez") test_with_type_info(t, b8, "deez") test_with_type_info(t, b16, "deez") test_with_type_info(t, b32, "deez") test_with_type_info(t, b64, "deez") test_with_type_info(t, time.Duration, "") test_with_type_info(t, time.Duration, "10r") test_with_type_info(t, time.Duration, "10b") test_with_type_info(t, time.Duration, "10c") } @test ini_unmarshal_test :: proc(t: ^te.T) { test :: proc(t: ^te.T, $T: typeid, data: string, expected: T, should_fail : Ini_Unmarshal_Error = nil) -> T { val: T err := ini_unmarshal(data, &val) te.expectf(t, reflect.eq(err, should_fail), "Expected %v to be equal to %v", err, should_fail) if should_fail == nil { te.expectf(t, reflect.eq(val, expected), "Expected %v to be equal to %v from data %v", val, expected, data) } else { #partial switch e in err { case Ini_Missing_Category: delete(string(e)) case Ini_Missing_Field: delete(string(e)) case: } } return val } Basic :: struct { hi: struct { enable0: bool, enable1: b8, enable2: b16, enable3: b32, enable4: b64, }, } test(t, Basic, "[hi]\n", Basic{}) test(t, Basic, "[hi]\nenable1 = true\nenable4 = false", Basic{hi = {enable1 = true}}) Attributes :: struct { named: struct {enable_stuff: bool `ini:"name=enable"`} `ini:"name=not_named"`, required: struct {enable: bool `ini:"required"`} `ini:"required"`, } test(t, Attributes, ` [not_named] enable = true [required] enable = true `, Attributes{named = {enable_stuff = true}, required = {enable = true}}) test(t, Attributes, ` `, Attributes{}, Ini_Missing_Category("required")) test(t, Attributes, ` [required] empyt = hi `, Attributes{}, Ini_Missing_Field("enable")) Defaults :: struct { defaults: struct { enable: bool `ini:"default=true"`, str: string `ini:"default=hello"`, duration: time.Duration `ini:"default=4h"`, }, } defaults := test(t, Defaults, "", Defaults{defaults = {enable = true, str = "hello", duration = time.Hour * 4}}) delete(defaults.defaults.str) }