ezs/store/ini.odin

398 lines
15 KiB
Odin

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)
}