diff options
| author | gingerBill <bill@gingerbill.org> | 2024-06-04 19:08:03 +0100 |
|---|---|---|
| committer | gingerBill <bill@gingerbill.org> | 2024-06-04 19:08:03 +0100 |
| commit | f4dd48aa5d2d94b0daa72e4b51aae77774c49820 (patch) | |
| tree | 55b714c62203eab2afc6a21d17d2f1c06b2a5cf6 | |
| parent | 3b7100f8e5cdf325a51926ba02f785a5443b0415 (diff) | |
Add `core:flags`
Based on the Feoramund's original package
| -rw-r--r-- | core/flags/LICENSE | 28 | ||||
| -rw-r--r-- | core/flags/README.md | 124 | ||||
| -rw-r--r-- | core/flags/constants.odin | 15 | ||||
| -rw-r--r-- | core/flags/conversion.odin | 157 | ||||
| -rw-r--r-- | core/flags/doc.odin | 92 | ||||
| -rw-r--r-- | core/flags/errors.odin | 29 | ||||
| -rw-r--r-- | core/flags/parsing.odin | 86 | ||||
| -rw-r--r-- | core/flags/usage.odin | 139 | ||||
| -rw-r--r-- | core/flags/util.odin | 189 | ||||
| -rw-r--r-- | core/flags/validation.odin | 45 |
10 files changed, 904 insertions, 0 deletions
diff --git a/core/flags/LICENSE b/core/flags/LICENSE new file mode 100644 index 000000000..5357227be --- /dev/null +++ b/core/flags/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Feoramund, Ginger Bill + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/core/flags/README.md b/core/flags/README.md new file mode 100644 index 000000000..37dc1e9e4 --- /dev/null +++ b/core/flags/README.md @@ -0,0 +1,124 @@ +# `core:flags` + +`core:flags` is a complete command-line argument parser for the Odin programming +language. + +It works by using Odin's run-time type information to determine where and how +to store data on a struct provided by the user. Type conversion is handled +automatically and errors are reported with useful messages. + +## Struct Tags + +Users of the `encoding/json` package may be familiar with using tags to +annotate struct metadata. The same technique is used here to annotate where +arguments should go and which are required. + +Under the `flags` tag: + + - `name=S`, alias a struct field to `S` + - `pos=N`, place positional argument `N` into this field + - `hidden`, hide this field from the usage documentation + - `required`, cause verification to fail if this argument is not set + +There is also the `usage` tag, which is a plain string to be printed alongside +the flag in the usage output. + +## Syntax + +Arguments are treated differently on how they're formatted. The format is +similar to the Odin binary's way of handling compiler flags. + +``` +type handling +------------ ------------------------ +<positional> depends on struct layout +-<flag> set a bool to true +-<flag:option> set flag to option +-<flag=option> set flag to option, alternative syntax +-<map>:<key>=<value> set map[key] to value +``` + +## Complete Example + +```odin +package main + +import "core:fmt" +import "core:mem" +import "core:os" +import "core:path/filepath" + +import "core:flags" + +main :: proc() { + Options :: struct { + file: string `flags:"pos=0,required" usage:"input file"`, + out: string `flags:"pos=1" usage:"output file"`, + retry_count: uint `flags:"name=retries" usage:"times to retry process"`, + debug: bool `flags:"hidden" usage:"print debug info"`, + collection: map[string]string `usage:"path aliases"`, + } + + opt: Options + program: string + args: []string + + switch len(os.args) { + case 0: + flags.print_usage(&opt) + os.exit(0) + case: + program = filepath.base(os.args[0]) + args = os.args[1:] + } + + err := flags.parse(&opt, args) + + switch subtype in err { + case mem.Allocator_Error: + fmt.println("allocation error:", subtype) + os.exit(1) + case flags.Parse_Error: + fmt.println(subtype.message) + os.exit(1) + case flags.Validation_Error: + fmt.println(subtype.message) + os.exit(1) + case flags.Help_Request: + flags.print_usage(&opt, program) + os.exit(0) + } + + fmt.printf("%#v\n", opt) +} +``` + +``` +$ ./odin-flags +required argument `file` was not set + +$ ./odin-flags -help + +Usage: + odin-flags file [out] [-collection] [-retries] +Flags: + -file:<string> input file + -out:<string> output file + -collection:<string>=<string> path aliases + -retries:<uint> times to retry process + +$ ./odin-flags -retries:-3 +unable to set `retries` of type uint to `-3` + +$ ./odin-flags data -retries:3 -collection:core=./core -collection:runtime=./runtime +Options{ + file = "data", + out = "", + retry_count = 3, + debug = false, + collection = map[ + core = "./core", + runtime = "./runtime", + ], +} +``` diff --git a/core/flags/constants.odin b/core/flags/constants.odin new file mode 100644 index 000000000..d5df64a29 --- /dev/null +++ b/core/flags/constants.odin @@ -0,0 +1,15 @@ +package flags + +TAG_FLAGS :: "flags" +SUBTAG_NAME :: "name" +SUBTAG_POS :: "pos" +SUBTAG_REQUIRED :: "required" +SUBTAG_HIDDEN :: "hidden" + +TAG_USAGE :: "usage" + +MINIMUM_SPACING :: 4 +UNDOCUMENTED_FLAG :: "<This flag has not been documented yet.>" + +HARD_CODED_HELP_FLAG :: "help" +HARD_CODED_HELP_FLAG_SHORT :: "h" diff --git a/core/flags/conversion.odin b/core/flags/conversion.odin new file mode 100644 index 000000000..226250228 --- /dev/null +++ b/core/flags/conversion.odin @@ -0,0 +1,157 @@ +package flags + +import "base:intrinsics" +import "base:runtime" +import "core:fmt" +import "core:mem" +import "core:reflect" + +_, _, _, _, _ :: intrinsics, runtime, fmt, mem, reflect + +// Add a positional argument to a data struct, checking for specified +// positionals first before adding it to a fallback field. +add_positional :: proc(data: ^$T, index: int, arg: string) -> Error { + field, has_pos_assigned := get_field_by_pos(data, index) + + if !has_pos_assigned { + when !intrinsics.type_has_field(T, SUBTAG_POS) { + return Parse_Error { + .Extra_Pos, + fmt.tprintf("got extra positional argument `%s` with nowhere to store it", arg), + } + } + + // Fall back to adding it to a dynamic array named `pos`. + field = reflect.struct_field_by_name(T, SUBTAG_POS) + assert(field.type != nil, "this should never happen") + } + + ptr := cast(rawptr)(uintptr(data) + field.offset) + if !parse_and_set_pointer_by_type(ptr, arg, field.type) { + return Parse_Error { + .Bad_Type, + fmt.tprintf("unable to set positional %i (%s) of type %v to `%s`", index, field.name, field.type, arg), + } + } + + return nil +} + +// Set a `-flag` argument. +set_flag :: proc(data: ^$T, name: string) -> Error { + // We make a special case for help requests. + switch name { + case HARD_CODED_HELP_FLAG: + fallthrough + case HARD_CODED_HELP_FLAG_SHORT: + return Help_Request{} + } + + field := get_field_by_name(data, name) or_return + + #partial switch t in field.type.variant { + case runtime.Type_Info_Boolean: + ptr := cast(^bool)(uintptr(data) + field.offset) + ptr^ = true + case: + return Parse_Error { + .Bad_Type, + fmt.tprintf("unable to set `%s` of type %v to true", name, field.type), + } + } + + return nil +} + +// Set a `-flag:option` argument. +set_option :: proc(data: ^$T, name, option: string) -> Error { + field := get_field_by_name(data, name) or_return + + // Guard against incorrect syntax. + #partial switch t in field.type.variant { + case runtime.Type_Info_Map: + return Parse_Error { + .Missing_Value, + fmt.tprintf("unable to set `%s` of type %v to `%s`, are you missing an `=`?", name, field.type, option), + } + } + + ptr := rawptr(uintptr(data) + field.offset) + if !parse_and_set_pointer_by_type(ptr, option, field.type) { + return Parse_Error { + .Bad_Type, + fmt.tprintf("unable to set `%s` of type %v to `%s`", name, field.type, option), + } + } + + return nil +} + +// Set a `-map:key=value` argument. +set_key_value :: proc(data: ^$T, name, key, value: string) -> Error { + field := get_field_by_name(data, name) or_return + + #partial switch t in field.type.variant { + case runtime.Type_Info_Map: + if !reflect.is_string(t.key) { + return Parse_Error { + .Bad_Type, + fmt.tprintf("`%s` must be a map[string]", name), + } + } + + key := key + key_ptr := rawptr(&key) + key_cstr: cstring + if reflect.is_cstring(t.key) { + key_cstr = cstring(raw_data(key)) + key_ptr = &key_cstr + } + + raw_map := (^runtime.Raw_Map)(uintptr(data) + field.offset) + + hash := t.map_info.key_hasher(key_ptr, runtime.map_seed(raw_map^)) + + backing_alloc := false + elem_backing: []byte + value_ptr: rawptr + + if raw_map.allocator.procedure == nil { + raw_map.allocator = context.allocator + } else { + value_ptr = runtime.__dynamic_map_get(raw_map, + t.map_info, + hash, + key_ptr, + ) + } + + if value_ptr == nil { + elem_backing = mem.alloc_bytes(t.value.size, t.value.align) or_return + backing_alloc = true + value_ptr = raw_data(elem_backing) + } + + if !parse_and_set_pointer_by_type(value_ptr, value, t.value) { + break + } + + if backing_alloc { + runtime.__dynamic_map_set(raw_map, + t.map_info, + hash, + key_ptr, + value_ptr, + ) + + delete(elem_backing) + } + + return nil + } + + return Parse_Error { + .Bad_Type, + fmt.tprintf("unable to set `%s` of type %v with key=value `%s` = `%s`", name, field.type, key, value), + } +} diff --git a/core/flags/doc.odin b/core/flags/doc.odin new file mode 100644 index 000000000..c842050aa --- /dev/null +++ b/core/flags/doc.odin @@ -0,0 +1,92 @@ +/* +package flags implements a command-line argument parser. + +It works by using Odin's run-time type information to determine where and how +to store data on a struct provided by the user. Type conversion is handled +automatically and errors are reported with useful messages. + + +Command-Line Syntax: + +Arguments are treated differently on how they're formatted. The format is +similar to the Odin binary's way of handling compiler flags. + +``` +type handling +------------ ------------------------ +<positional> depends on struct layout +-<flag> set a bool true +-<flag:option> set flag to option +-<flag=option> set flag to option, alternative syntax +-<map>:<key>=<value> set map[key] to value +``` + + +Struct Tags: + +Users of the `encoding/json` package may be familiar with using tags to +annotate struct metadata. The same technique is used here to annotate where +arguments should go and which are required. + +Under the `args` tag: + + - `name=S`, alias a struct field to `S` + - `pos=N`, place positional argument `N` into this field + - `hidden`, hide this field from the usage documentation + - `required`, cause verification to fail if this argument is not set + +There is also the `usage` tag, which is a plain string to be printed alongside +the flag in the usage output. + + +Supported Field Datatypes: + +- all `bool`s +- all `int`s +- all `float`s +- `string`, `cstring` +- `rune` +- `dynamic` arrays with element types of the above +- `map[string]`s with value types of the above + + +Validation: + +The parser will ensure `required` arguments are set. This is on by default. + + +Strict: + +The parser will return on the first error and stop parsing. This is on by +default. Otherwise, all arguments that can be parsed, will be, and only the +last error is returned. + + +Help: + +By default, `-h` and `-help` are reserved flags which raise their own error +type when set, allowing the program to handle the request differently from +other errors. + + +Example: + +```odin + Options :: struct { + file: string `args:"pos=0,required" usage:"input file"`, + out: string `args:"pos=1" usage:"output file"`, + retry_count: uint `args:"name=retries" usage:"times to retry process"`, + debug: bool `args:"hidden" usage:"print debug info"`, + collection: map[string]string `usage:"path aliases"`, + } + + opt: Options + flags.parse(&opt, { + "main.odin", + "-retries:3", + "-collection:core=./core", + "-debug", + }, validate_args = true, strict = true) +``` +*/ +package flags diff --git a/core/flags/errors.odin b/core/flags/errors.odin new file mode 100644 index 000000000..6acd26c20 --- /dev/null +++ b/core/flags/errors.odin @@ -0,0 +1,29 @@ +package flags + +import "base:runtime" + +Parse_Error_Type :: enum { + None, + Extra_Pos, + Bad_Type, + Missing_Field, + Missing_Value, +} + +Parse_Error :: struct { + type: Parse_Error_Type, + message: string, +} + +Validation_Error :: struct { + message: string, +} + +Help_Request :: distinct bool + +Error :: union { + runtime.Allocator_Error, + Parse_Error, + Validation_Error, + Help_Request, +} diff --git a/core/flags/parsing.odin b/core/flags/parsing.odin new file mode 100644 index 000000000..68f3ea56e --- /dev/null +++ b/core/flags/parsing.odin @@ -0,0 +1,86 @@ +package flags + +import "core:strings" +_ :: strings + +@(private) +parse_one_arg :: proc(data: ^$T, arg: string, pos: ^int, set_args: ^[dynamic]string) -> (err: Error) { + arg := arg + + if strings.has_prefix(arg, "-") { + arg = arg[1:] + + if colon := strings.index_byte(arg, ':'); colon != -1 { + flag := arg[:colon] + arg = arg[1 + colon:] + + if equals := strings.index_byte(arg, '='); equals != -1 { + // -map:key=value + key := arg[:equals] + value := arg[1 + equals:] + set_key_value(data, flag, key, value) or_return + append(set_args, flag) + } else { + // -flag:option + set_option(data, flag, arg) or_return + append(set_args, flag) + } + + } else if equals := strings.index_byte(arg, '='); equals != -1 { + // -flag=option, alternative syntax + flag := arg[:equals] + arg = arg[1 + equals:] + + set_option(data, flag, arg) or_return + append(set_args, flag) + } else { + // -flag + set_flag(data, arg) or_return + append(set_args, arg) + } + + } else { + // positional + err = add_positional(data, pos^, arg) + pos^ += 1 + } + + return +} + +// Parse a slice of command-line arguments into an annotated struct. +// +// If `validate_args` is set, an error will be returned if all required +// arguments are not set. This step is only completed if there were no errors +// from parsing. +// +// If `strict` is set, an error will cause parsing to stop and the procedure +// will return with the message. Otherwise, parsing will continue and only the +// last error will be returned. +parse :: proc(data: ^$T, args: []string, validate_args: bool = true, strict: bool = true) -> (err: Error) { + // For checking required arguments. + set_args: [dynamic]string + defer delete(set_args) + + // Positional argument tracker. + pos := 0 + + if strict { + for arg in args { + parse_one_arg(data, arg, &pos, &set_args) or_return + } + } else { + for arg in args { + this_error := parse_one_arg(data, arg, &pos, &set_args) + if this_error != nil { + err = this_error + } + } + } + + if err == nil && validate_args { + return validate(data, pos, set_args[:]) + } + + return err +} diff --git a/core/flags/usage.odin b/core/flags/usage.odin new file mode 100644 index 000000000..ebf440393 --- /dev/null +++ b/core/flags/usage.odin @@ -0,0 +1,139 @@ +package flags + +import "base:runtime" +import "core:fmt" +import "core:io" +import "core:os" +import "core:reflect" +import "core:slice" +import "core:strconv" +import "core:strings" + +_, _, _, _, _, _, _, _ :: runtime, fmt, io, os, reflect, slice, strconv, strings + +// Write out the documentation for the command-line arguments. +write_usage :: proc(out: io.Writer, data: ^$T, program: string = "") { + Flag :: struct { + name: string, + usage: string, + name_with_type: string, + pos: int, + is_positional: bool, + is_required: bool, + is_boolean: bool, + is_hidden: bool, + } + + sort_flags :: proc(a, b: Flag) -> slice.Ordering { + if a.is_positional && b.is_positional { + return slice.cmp(a.pos, b.pos) + } + + if a.is_required && !b.is_required { + return .Less + } else if !a.is_required && b.is_required { + return .Greater + } + + if a.is_positional && !b.is_positional { + return .Less + } else if b.is_positional && !a.is_positional { + return .Greater + } + + return slice.cmp(a.name, b.name) + } + + flags: [dynamic]Flag + defer delete(flags) + + longest_flag_length: int + + for field in reflect.struct_fields_zipped(T) { + flag: Flag + + flag.name = get_field_name(field) + #partial switch t in field.type.variant { + case runtime.Type_Info_Map: + flag.name_with_type = fmt.tprintf("%s:<%v>=<%v>", flag.name, t.key.id, t.value.id) + case runtime.Type_Info_Dynamic_Array: + flag.name_with_type = fmt.tprintf("%s:<%v, ...>", flag.name, t.elem.id) + case: + flag.name_with_type = fmt.tprintf("%s:<%v>", flag.name, field.type.id) + } + + if usage, ok := reflect.struct_tag_lookup(field.tag, TAG_USAGE); ok { + flag.usage = usage + } else { + flag.usage = UNDOCUMENTED_FLAG + } + + if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_ARGS); ok { + if pos_str, is_pos := get_struct_subtag(args_tag, SUBTAG_POS); is_pos { + flag.is_positional = true + if pos, ok := strconv.parse_int(pos_str); ok && pos >= 0 { + flag.pos = pos + } else { + fmt.panicf("%v has incorrect pos subtag specifier `%s`", typeid_of(T), pos_str) + } + } + if _, is_required := get_struct_subtag(args_tag, SUBTAG_REQUIRED); is_required { + flag.is_required = true + } + if reflect.type_kind(field.type.id) == .Boolean { + flag.is_boolean = true + } + if _, is_hidden := get_struct_subtag(args_tag, SUBTAG_HIDDEN); is_hidden { + flag.is_hidden = true + } + } + + if !flag.is_hidden { + longest_flag_length = max(longest_flag_length, len(flag.name_with_type)) + } + + append(&flags, flag) + } + + slice.sort_by_cmp(flags[:], sort_flags) + + if len(program) > 0 { + fmt.wprintf(out, "Usage:\n\t%s", program) + + for flag in flags { + if flag.is_hidden { + continue + } + + io.write_byte(out, ' ') + + if flag.name == SUBTAG_POS { + io.write_string(out, "...") + continue + } + + if !flag.is_required { io.write_byte(out, '[') } + if !flag.is_positional { io.write_byte(out, '-') } + io.write_string(out, flag.name) + if !flag.is_required { io.write_byte(out, ']') } + } + io.write_byte(out, '\n') + } + + fmt.wprintln(out, "Flags:") + for flag in flags { + if flag.is_hidden { + continue + } + + spacing := strings.repeat(" ", + (MINIMUM_SPACING + longest_flag_length) - len(flag.name_with_type), + context.temp_allocator) + fmt.wprintf(out, "\t-%s%s%s\n", flag.name_with_type, spacing, flag.usage) + } +} + +// Print out the documentation for the command-line arguments. +print_usage :: proc(data: ^$T, program: string = "") { + write_usage(os.stream_from_handle(os.stdout), data, program) +} diff --git a/core/flags/util.odin b/core/flags/util.odin new file mode 100644 index 000000000..fdce20272 --- /dev/null +++ b/core/flags/util.odin @@ -0,0 +1,189 @@ +package flags + +import "base:runtime" +import "core:fmt" +import "core:mem" +import "core:reflect" +import "core:strconv" +import "core:strings" +import "core:unicode/utf8" + +_, _, _, _, _, _, _ :: runtime, fmt, mem, reflect, strconv, strings, utf8 + +@(private) +parse_and_set_pointer_by_type :: proc(ptr: rawptr, value: string, ti: ^runtime.Type_Info) -> bool { + set_bool :: proc(ptr: rawptr, $T: typeid, str: string) -> bool { + (^T)(ptr)^ = (T)(strconv.parse_bool(str) or_return) + return true + } + + set_i128 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool { + value := strconv.parse_i128(str) or_return + if value > cast(i128)max(T) || value < cast(i128)min(T) { + return false + } + (^T)(ptr)^ = (T)(value) + return true + } + + set_u128 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool { + value := strconv.parse_u128(str) or_return + if value > cast(u128)max(T) { + return false + } + (^T)(ptr)^ = (T)(value) + return true + } + + set_f64 :: proc(ptr: rawptr, $T: typeid, str: string) -> bool { + (^T)(ptr)^ = (T)(strconv.parse_f64(str) or_return) + return true + } + + a := any{ptr, ti.id} + + #partial switch t in ti.variant { + case runtime.Type_Info_Dynamic_Array: + ptr := (^runtime.Raw_Dynamic_Array)(ptr) + + // Try to convert the value first. + elem_backing, mem_err := mem.alloc_bytes(t.elem.size, t.elem.align) + if mem_err != nil { + return false + } + defer delete(elem_backing) + parse_and_set_pointer_by_type(raw_data(elem_backing), value, t.elem) or_return + + runtime.__dynamic_array_resize(ptr, t.elem.size, t.elem.align, ptr.len + 1) or_return + subptr := cast(rawptr)(uintptr(ptr.data) + uintptr((ptr.len - 1) * t.elem.size)) + mem.copy(subptr, raw_data(elem_backing), len(elem_backing)) + + case runtime.Type_Info_Boolean: + switch b in a { + case bool: set_bool(ptr, bool, value) or_return + case b8: set_bool(ptr, b8, value) or_return + case b16: set_bool(ptr, b16, value) or_return + case b32: set_bool(ptr, b32, value) or_return + case b64: set_bool(ptr, b64, value) or_return + } + + case runtime.Type_Info_Rune: + r := utf8.rune_at_pos(value, 0) + if r == utf8.RUNE_ERROR { return false } + (^rune)(ptr)^ = r + + case runtime.Type_Info_String: + switch s in a { + case string: (^string)(ptr)^ = value + case cstring: (^cstring)(ptr)^ = strings.clone_to_cstring(value) + } + case runtime.Type_Info_Integer: + switch i in a { + case int: set_i128(ptr, int, value) or_return + case i8: set_i128(ptr, i8, value) or_return + case i16: set_i128(ptr, i16, value) or_return + case i32: set_i128(ptr, i32, value) or_return + case i64: set_i128(ptr, i64, value) or_return + case i128: set_i128(ptr, i128, value) or_return + case i16le: set_i128(ptr, i16le, value) or_return + case i32le: set_i128(ptr, i32le, value) or_return + case i64le: set_i128(ptr, i64le, value) or_return + case i128le: set_i128(ptr, i128le, value) or_return + case i16be: set_i128(ptr, i16be, value) or_return + case i32be: set_i128(ptr, i32be, value) or_return + case i64be: set_i128(ptr, i64be, value) or_return + case i128be: set_i128(ptr, i128be, value) or_return + + case uint: set_u128(ptr, uint, value) or_return + case uintptr: set_u128(ptr, uintptr, value) or_return + case u8: set_u128(ptr, u8, value) or_return + case u16: set_u128(ptr, u16, value) or_return + case u32: set_u128(ptr, u32, value) or_return + case u64: set_u128(ptr, u64, value) or_return + case u128: set_u128(ptr, u128, value) or_return + case u16le: set_u128(ptr, u16le, value) or_return + case u32le: set_u128(ptr, u32le, value) or_return + case u64le: set_u128(ptr, u64le, value) or_return + case u128le: set_u128(ptr, u128le, value) or_return + case u16be: set_u128(ptr, u16be, value) or_return + case u32be: set_u128(ptr, u32be, value) or_return + case u64be: set_u128(ptr, u64be, value) or_return + case u128be: set_u128(ptr, u128be, value) or_return + } + case runtime.Type_Info_Float: + switch f in a { + case f16: set_f64(ptr, f16, value) or_return + case f32: set_f64(ptr, f32, value) or_return + case f64: set_f64(ptr, f64, value) or_return + + case f16le: set_f64(ptr, f16le, value) or_return + case f32le: set_f64(ptr, f32le, value) or_return + case f64le: set_f64(ptr, f64le, value) or_return + + case f16be: set_f64(ptr, f16be, value) or_return + case f32be: set_f64(ptr, f32be, value) or_return + case f64be: set_f64(ptr, f64be, value) or_return + } + case: + return false + } + + return true +} + +@(private) +get_struct_subtag :: proc(tag, id: string) -> (value: string, ok: bool) { + 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 "", false +} + +@(private) +get_field_name :: proc(field: reflect.Struct_Field) -> string { + if args_tag, ok := reflect.struct_tag_lookup(field.tag, TAG_FLAGS); ok { + if name_subtag, name_ok := get_struct_subtag(args_tag, SUBTAG_NAME); name_ok { + return name_subtag + } + } + + return field.name +} + +// Get a struct field by its field name or "name" subtag. +// NOTE: `Error` uses the `context.temp_allocator` to give context about the error message +get_field_by_name :: proc(data: ^$T, name: string) -> (field: reflect.Struct_Field, err: Error) { + for field in reflect.struct_fields_zipped(T) { + if get_field_name(field) == name { + return field, nil + } + } + + return {}, Parse_Error { + .Missing_Field, + fmt.tprintf("unable to find argument by name `%s`", name), + } +} + +// Get a struct field by its "pos" subtag. +get_field_by_pos :: proc(data: ^$T, index: int) -> (field: reflect.Struct_Field, ok: bool) { + fields := reflect.struct_fields_zipped(T) + + for field in fields { + args_tag := reflect.struct_tag_lookup(field.tag, TAG_FLAGS) or_continue + pos_subtag := get_struct_subtag(args_tag, SUBTAG_POS) or_continue + value := strconv.parse_int(pos_subtag) or_continue + if value == index { + return field, true + } + } + + return {}, false +} diff --git a/core/flags/validation.odin b/core/flags/validation.odin new file mode 100644 index 000000000..c5cad8c7a --- /dev/null +++ b/core/flags/validation.odin @@ -0,0 +1,45 @@ +package flags + +import "core:fmt" +import "core:reflect" +import "core:strconv" + +_ :: fmt +_ :: reflect +_ :: strconv + +// Validate that all the required arguments are set. +validate :: proc(data: ^$T, max_pos: int, set_args: []string) -> Error { + fields := reflect.struct_fields_zipped(T) + + check_fields: for field in fields { + tag := reflect.struct_tag_lookup(field.tag, TAG_ARGS) or_continue + if _, ok := get_struct_subtag(tag, SUBTAG_REQUIRED); ok { + was_set := false + + // Check if it was set by name. + check_set_args: for set_arg in set_args { + if get_field_name(field) == set_arg { + was_set = true + break check_set_args + } + } + + // Check if it was set by position. + if pos, has_pos := get_struct_subtag(tag, SUBTAG_POS); has_pos { + value, value_ok := strconv.parse_int(pos) + if value < max_pos { + was_set = true + } + } + + if !was_set { + return Validation_Error { + fmt.tprintf("required argument `%s` was not set", field.name), + } + } + } + } + + return nil +} |