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 { bounded_int :: proc(value, min, max: i128) -> (result: i128, ok: bool) { return value, min <= value && value <= max } bounded_uint :: proc(value, max: u128) -> (result: u128, ok: bool) { return value, value <= max } #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_Float: value := strconv.parse_f64(str) or_return switch type_info.id { case f16: (cast( ^f16)ptr)^ = cast( f16) value case f32: (cast( ^f32)ptr)^ = cast( f32) value case f64: (cast( ^f64)ptr)^ = value case f16be: (cast(^f16be)ptr)^ = cast(f16be) value case f32be: (cast(^f32be)ptr)^ = cast(f32be) value case f64be: (cast(^f64be)ptr)^ = cast(f64be) value case f16le: (cast(^f16le)ptr)^ = cast(f16le) value case f32le: (cast(^f32le)ptr)^ = cast(f32le) value case f64le: (cast(^f64le)ptr)^ = cast(f64le) value } case runtime.Type_Info_Integer: if type_info_variant.signed { value := strconv.parse_i128(str) or_return switch type_info.id { case i8: (cast( ^i8)ptr)^ = cast( i8) bounded_int(value, cast(i128)min( i8), cast(i128)max( i8)) or_return case i16: (cast( ^i16)ptr)^ = cast( i16) bounded_int(value, cast(i128)min( i16), cast(i128)max( i16)) or_return case i32: (cast( ^i32)ptr)^ = cast( i32) bounded_int(value, cast(i128)min( i32), cast(i128)max( i32)) or_return case i64: (cast( ^i64)ptr)^ = cast( i64) bounded_int(value, cast(i128)min( i64), cast(i128)max( i64)) or_return case i128: (cast(^i128)ptr)^ = bounded_int(value, min(i128), max(i128)) or_return case int: (cast( ^int)ptr)^ = cast( int) bounded_int(value, cast(i128)min( int), cast(i128)max( int)) or_return case i16be: (cast( ^i16be)ptr)^ = cast( i16be) bounded_int(value, cast(i128)min( i16be), cast(i128)max( i16be)) or_return case i32be: (cast( ^i32be)ptr)^ = cast( i32be) bounded_int(value, cast(i128)min( i32be), cast(i128)max( i32be)) or_return case i64be: (cast( ^i64be)ptr)^ = cast( i64be) bounded_int(value, cast(i128)min( i64be), cast(i128)max( i64be)) or_return case i128be: (cast(^i128be)ptr)^ = cast(i128be) bounded_int(value, cast(i128)min(i128be), cast(i128)max(i128be)) or_return case i16le: (cast( ^i16le)ptr)^ = cast( i16le) bounded_int(value, cast(i128)min( i16le), cast(i128)max( i16le)) or_return case i32le: (cast( ^i32le)ptr)^ = cast( i32le) bounded_int(value, cast(i128)min( i32le), cast(i128)max( i32le)) or_return case i64le: (cast( ^i64le)ptr)^ = cast( i64le) bounded_int(value, cast(i128)min( i64le), cast(i128)max( i64le)) or_return case i128le: (cast(^i128le)ptr)^ = cast(i128le) bounded_int(value, cast(i128)min(i128le), cast(i128)max(i128le)) or_return } } else { value := strconv.parse_u128(str) or_return switch type_info.id { case u8: (cast( ^u8)ptr)^ = cast( u8) bounded_uint(value, cast(u128)max( u8)) or_return case u16: (cast( ^u16)ptr)^ = cast( u16) bounded_uint(value, cast(u128)max( u16)) or_return case u32: (cast( ^u32)ptr)^ = cast( u32) bounded_uint(value, cast(u128)max( u32)) or_return case u64: (cast( ^u64)ptr)^ = cast( u64) bounded_uint(value, cast(u128)max( u64)) or_return case u128: (cast( ^u128)ptr)^ = bounded_uint(value, max( u128)) or_return case uint: (cast( ^uint)ptr)^ = cast( uint) bounded_uint(value, cast(u128)max( uint)) or_return case uintptr: (cast(^uintptr)ptr)^ = cast(uintptr) bounded_uint(value, cast(u128)max(uintptr)) or_return case u16be: (cast( ^u16be)ptr)^ = cast( u16be) bounded_uint(value, cast(u128)max( u16be)) or_return case u32be: (cast( ^u32be)ptr)^ = cast( u32be) bounded_uint(value, cast(u128)max( u32be)) or_return case u64be: (cast( ^u64be)ptr)^ = cast( u64be) bounded_uint(value, cast(u128)max( u64be)) or_return case u128be: (cast(^u128be)ptr)^ = cast(u128be) bounded_uint(value, cast(u128)max(u128be)) or_return case u16le: (cast( ^u16le)ptr)^ = cast( u16le) bounded_uint(value, cast(u128)max( u16le)) or_return case u32le: (cast( ^u32le)ptr)^ = cast( u32le) bounded_uint(value, cast(u128)max( u32le)) or_return case u64le: (cast( ^u64le)ptr)^ = cast( u64le) bounded_uint(value, cast(u128)max( u64le)) or_return case u128le: (cast(^u128le)ptr)^ = cast(u128le) bounded_uint(value, cast(u128)max(u128le)) or_return } } 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 { fmt.panicf("INI: Invalid type for INI Unmarshaler '%v'", type_info) } } 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, i8, "-3", -3) test_with_type_info(t, i16, "-3", -3) test_with_type_info(t, i32, "-3", -3) test_with_type_info(t, i64, "-3", -3) test_with_type_info(t, i128, "-3", -3) test_with_type_info(t, int, "-3", -3) test_with_type_info(t, u8, "3", 3) test_with_type_info(t, u16, "3", 3) test_with_type_info(t, u32, "3", 3) test_with_type_info(t, u64, "3", 3) test_with_type_info(t, u128, "3", 3) test_with_type_info(t, uint, "3", 3) test_with_type_info(t, uintptr, "3", 3) test_with_type_info(t, f16, "0.125", 0.125) test_with_type_info(t, f32, "0.125", 0.125) test_with_type_info(t, f64, "0.125", 0.125) 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) }