From e8eda2103827f51d13f2e7647df2ac93a62c556f Mon Sep 17 00:00:00 2001 From: larpon <768942+larpon@users.noreply.github.com> Date: Sun, 14 Jul 2024 15:53:47 +0200 Subject: [PATCH] flag: add `flag.using[T]()!` that uses an existing instance of `T` (#21865) --- vlib/flag/README.md | 16 +++++++--- vlib/flag/flag_from_test.v | 55 ++++++++++++++++++++++++++++++++ vlib/flag/flag_to.v | 40 +++++++++++++++++------ vlib/flag/flag_to_misc_test.v | 6 ++-- vlib/flag/gnu_style_flags_test.v | 10 +++--- 5 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 vlib/flag/flag_from_test.v diff --git a/vlib/flag/README.md b/vlib/flag/README.md index 35e6c4673c..4530c82a42 100644 --- a/vlib/flag/README.md +++ b/vlib/flag/README.md @@ -58,10 +58,7 @@ fn main() { config, no_matches := flag.to_struct[Config](os.args, skip: 1)! if no_matches.len > 0 { - println('The following flags could not be mapped to any fields on the struct:') - for index in no_matches { - println('${index}: ${os.args[no_matches[index]]}') - } + println('The following flags could not be mapped to any fields on the struct: ${no_matches}') } if config.show_help { @@ -91,7 +88,7 @@ The 2 most useful functions in the module is `to_struct[T]()` and `to_doc[T]()`. ## `to_struct[T](...)` -`to_struct[T](input []string, config ParseConfig) !(T, []int)` maps flags found in `input` +`to_struct[T](input []string, config ParseConfig) !(T, []string)` maps flags found in `input` to *matching* fields on `T`. The matching is determined via hints that the user can specify with special field attributes. The matching is done in the following way: @@ -105,6 +102,15 @@ specify with special field attributes. The matching is done in the following way * To map a field *solely* to a short flag use `@[only: n]` * Short flags that repeats is mapped to fields via the attribute `@[repeats]` +A new instance of `T` is returned with fields assigned with values from any matching +input flags, along with an array of flags that could not be matched. + +## using[T](...) + +`using[T](defaults T, input []string, config ParseConfig) !(T, []string)` does the same as +`to_struct[T]()` but allows for passing in an existing instance of `T`, making it possible +to preserve existing field values that does not match any flags in `input`. + ## `to_doc[T](...)` `pub fn to_doc[T](dc DocConfig) !string` returns an auto-generated `string` with flag diff --git a/vlib/flag/flag_from_test.v b/vlib/flag/flag_from_test.v new file mode 100644 index 0000000000..7f82fd550e --- /dev/null +++ b/vlib/flag/flag_from_test.v @@ -0,0 +1,55 @@ +import flag + +const some_args_1 = ['--mix', '-m', 'ok', '-d', 'one', '--test=abc', '-d', 'two', '/path/to/a', + 'path/to/b'] + +struct Config { + am string @[only: m] + def_test string = 'def' @[long: test; short: t] + device []string @[short: d] + paths []string @[tail] +mut: + amount int = 1 + mix bool + mix_hard bool = true +} + +fn test_using() { + mut config := Config{ + mix_hard: false + amount: 8 + } + + config, _ = flag.using[Config](config, some_args_1)! + assert config.mix + assert config.mix_hard == false + assert config.am == 'ok' + assert config.def_test == 'abc' + assert config.device[0] == 'one' + assert config.device[1] == 'two' + assert config.amount == 8 + assert config.paths.len == 2 + assert config.paths[0] == '/path/to/a' + assert config.paths[1] == 'path/to/b' + + config.mix = false // is changed to true via `--mix` + config.mix_hard = true // should be kept as `true`, since no flags changed it + config.amount = 888 + + config2, _ := flag.using[Config](config, some_args_1)! + assert config2.mix + assert config2.mix_hard + assert config2.am == 'ok' + assert config2.def_test == 'abc' + assert config2.device[0] == 'one' + assert config2.device[1] == 'two' + assert config2.device[2] == 'one' // `config` already had items pushed from `some_args_1` so this grows when using `using[T](struct,...)` + assert config2.device[3] == 'two' + assert config2.device.len == 4 + assert config2.amount == 888 + assert config2.paths.len == 4 + assert config2.paths[0] == '/path/to/a' + assert config2.paths[1] == 'path/to/b' + assert config2.paths[2] == '/path/to/a' + assert config2.paths[3] == 'path/to/b' +} diff --git a/vlib/flag/flag_to.v b/vlib/flag/flag_to.v index e997299bfb..21c6d667af 100644 --- a/vlib/flag/flag_to.v +++ b/vlib/flag/flag_to.v @@ -297,13 +297,30 @@ fn (m map[string]FlagData) query_flag_with_name(name string) ?FlagData { } // to_struct returns `T` with field values sat to any matching flags in `input`. -pub fn to_struct[T](input []string, config ParseConfig) !(T, []int) { +// to_struct also returns any flags from `input`, in order of appearance, that could *not* be matched +// with any field on `T`. +pub fn to_struct[T](input []string, config ParseConfig) !(T, []string) { mut fm := FlagMapper{ config: config input: input } fm.parse[T]()! - st := fm.to_struct[T]()! + st := fm.to_struct[T](none)! + return st, fm.no_matches() +} + +// using returns `defaults` with field values overwritten with any matching flags in `input`. +// Any field that could *not* be matched with a flag will have the same value as the +// field(s) passed as `defaults`. +// using also returns any flags from `input`, in order of appearance, that could *not* be matched +// with any field on `T`. +pub fn using[T](defaults T, input []string, config ParseConfig) !(T, []string) { + mut fm := FlagMapper{ + config: config + input: input + } + fm.parse[T]()! + st := fm.to_struct[T](defaults)! return st, fm.no_matches() } @@ -321,10 +338,15 @@ pub fn to_doc[T](dc DocConfig) !string { return fm.to_doc(dc)! } -// no_matches returns an array of indicies from the `input` (usually `os.args`), -// that could not be matched against any fields. -pub fn (fm FlagMapper) no_matches() []int { - return fm.no_match +// no_matches returns any flags from the `input` array, in order of appearance, +// that could *not* be matched against any fields. +// no_matches should be called *after* `to_struct[T]()`. +pub fn (fm FlagMapper) no_matches() []string { + mut non_matching := []string{} + for i in fm.no_match { + non_matching << fm.input[i] + } + return non_matching } // parse parses `T` to an internal data representation. @@ -777,10 +799,10 @@ fn keep_at_max(str string, max int) string { return fitted } -// to_struct returns an instance of `T` that has the parsed flags from `input` mapped to the fields of struct `T`. -pub fn (fm FlagMapper) to_struct[T]() !T { +// to_struct returns `defaults` or a new instance of `T` that has the parsed flags from `input` mapped to the fields of struct `T`. +pub fn (fm FlagMapper) to_struct[T](defaults ?T) !T { // Generate T result - mut result := T{} + mut result := defaults or { T{} } $if T is $struct { struct_name := T.name diff --git a/vlib/flag/flag_to_misc_test.v b/vlib/flag/flag_to_misc_test.v index c94ce3735c..7273317b4b 100644 --- a/vlib/flag/flag_to_misc_test.v +++ b/vlib/flag/flag_to_misc_test.v @@ -136,7 +136,7 @@ fn test_flag_error_messages() { style: e_num ) { - assert no_matches == [0, 1] // index 0 = executable, index 1 = subcmd + assert no_matches == ['/path/to/exe', 'subcmd'] // index 0 = executable, index 1 = subcmd } } @@ -155,7 +155,7 @@ fn test_flag_error_messages() { } } if _, no_matches := flag.to_struct[LongConfig](gnu_args_error, style: .long) { - assert no_matches == [6] + assert no_matches == ['oo'] } else { assert false, 'flags should not have reached this assert' } @@ -166,6 +166,6 @@ fn test_flag_error_messages() { assert err.msg() == 'flag `--version=1.2.3` can not be assigned to bool field "show_version"' } if _, no_matches := flag.to_struct[IgnoreConfig](ignore_args_error, style: .long) { - assert no_matches == [1] + assert no_matches == ['--some-test=ouch'] } } diff --git a/vlib/flag/gnu_style_flags_test.v b/vlib/flag/gnu_style_flags_test.v index 79ce941dd1..2023282a07 100644 --- a/vlib/flag/gnu_style_flags_test.v +++ b/vlib/flag/gnu_style_flags_test.v @@ -40,9 +40,9 @@ fn test_pure_gnu_long_no_exe() { fn test_pure_gnu_long_with_tail() { config, no_matches := flag.to_struct[Config](exe_and_gnu_args_with_tail, skip: 1, style: .long)! - assert config.path == '/path/to/x' - assert exe_and_gnu_args_with_tail[no_matches[0]] == '/path/to/y' - assert exe_and_gnu_args_with_tail[no_matches[1]] == '/path/to/z' + assert config.path == '/path/to/x' // path is of type `string`, not `[]string` + assert no_matches[0] == '/path/to/y' + assert no_matches[1] == '/path/to/z' assert config.amount == 6 } @@ -51,8 +51,8 @@ fn test_pure_gnu_long_with_tail_no_exe() { a := exe_and_gnu_args_with_tail[1..] config, no_matches := flag.to_struct[Config](a, style: .long)! assert config.path == '/path/to/x' - assert a[no_matches[0]] == '/path/to/y' - assert a[no_matches[1]] == '/path/to/z' + assert no_matches[0] == '/path/to/y' + assert no_matches[1] == '/path/to/z' assert config.amount == 6 }