diff --git a/cmd/cmd.odin b/cmd/cmd.odin index c6d1dfc..37bc74b 100644 --- a/cmd/cmd.odin +++ b/cmd/cmd.odin @@ -1,11 +1,12 @@ package cmd -import "../deamon" +// import "../deamon" +import "../store" import "core:os" import "core:log" -import "core:hash/xxhash" -import "core:encoding/base64" +// import "core:hash/xxhash" +// import "core:encoding/base64" Options :: struct { deamon: bool, @@ -17,12 +18,15 @@ main :: proc() { d, _ := os.read_entire_file_or_err(os.args[1]) defer delete(d) - hash := xxhash.XXH3_128(d) - hash_bytes := transmute([size_of(hash)]u8)hash - data, _ := base64.encode(hash_bytes[:]) - base64.ENC_TABLE + config: store.Config + log.error(store.ini_unmarshal(string(d), &config), config) - log.infof("%q", data) + // hash := xxhash.XXH3_128(d) + // hash_bytes := transmute([size_of(hash)]u8)hash + // data, _ := base64.encode(hash_bytes[:]) + // base64.ENC_TABLE + // + // log.infof("%q", data) // diff --git a/store/ini.odin b/store/ini.odin index 77a833e..df683dc 100644 --- a/store/ini.odin +++ b/store/ini.odin @@ -2,13 +2,91 @@ 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" +// 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 { @@ -18,10 +96,216 @@ ini_unmarshal :: proc(data: string, v: ^$T, allocator := context.allocator) -> I 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_bool_default :: proc(field: reflect.Struct_Field) { - val, ok := reflect.struct_tag_lookup(field.tag, "ini") +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) }