flag: add flag.using[T]()! that uses an existing instance of T (#21865)

This commit is contained in:
larpon 2024-07-14 15:53:47 +02:00 committed by GitHub
parent 769e9147c3
commit e8eda21038
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 105 additions and 22 deletions

View File

@ -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

View File

@ -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'
}

View File

@ -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

View File

@ -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']
}
}

View File

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