From e348cce3d2e1e6637189202abb516468f8ac05a7 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Sat, 6 Sep 2025 02:46:28 +0200 Subject: [PATCH] test command validation --- Makefile | 2 +- command.go | 60 ++++---- command_test.go | 347 ++++++++++++++++++++++++++++++++++++++++++ commandline_test.go | 142 ++++++++--------- config_test.go | 12 +- debug.go | 8 + exec.go | 3 +- exec_test.go | 52 +++++-- go.mod | 2 +- go.sum | 10 +- iniparser.gen.go | 361 ++++++++++++++++++++++++++++++++++---------- notes.txt | 3 + reflect.go | 24 ++- reflect_test.go | 77 +++++++++- 14 files changed, 883 insertions(+), 220 deletions(-) create mode 100644 command_test.go diff --git a/Makefile b/Makefile index cb08378..e33492b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -SOURCES = $(shell find . -name "*.go" | grep -v iniparser.gen.go | grep -v docreflect.gen.go) +SOURCES = $(shell find . -name "*.go" | grep -v iniparser.gen.go | grep -v docreflect.gen.go | grep -v docreflect_test.go) default: build diff --git a/command.go b/command.go index 4c276a5..0a78ddc 100644 --- a/command.go +++ b/command.go @@ -9,7 +9,7 @@ import ( "strings" ) -var commandNameExpression = regexp.MustCompile("^[a-zA-Z_][a-zA-Z_0-9]*$") +var commandNameExpression = regexp.MustCompile("^[a-zA-Z_][a-zA-Z_0-9-]*$") func wrap(impl any) Cmd { cmd, ok := impl.(Cmd) @@ -120,10 +120,6 @@ func validateImpl(cmd Cmd, conf Config) error { } func validateCommandTree(cmd Cmd, conf Config) error { - if cmd.helpFor != nil { - return nil - } - if cmd.version != "" { return nil } @@ -171,7 +167,7 @@ func validateCommandTree(cmd Cmd, conf Config) error { } if err := validateCommandTree(s, conf); err != nil { - return fmt.Errorf("%s: %w", s.name, err) + return fmt.Errorf("%s: %w", cmd.name, err) } } @@ -227,10 +223,6 @@ func validateShortFormsTree(cmd Cmd) (map[string]string, map[string]string, erro } } - if len(cmd.shortForms)%2 != 0 { - return nil, nil, fmt.Errorf("unassigned short form: %s", cmd.shortForms[len(cmd.shortForms)-1]) - } - mf := mapFields(cmd.impl) _, helpDefined := mf["help"] for i := 0; i < len(cmd.shortForms); i += 2 { @@ -304,26 +296,6 @@ func validateCommand(cmd Cmd, conf Config) error { return nil } -func insertHelpOption(names []string) []string { - for _, n := range names { - if n == "help" { - return names - } - } - - return append(names, "help") -} - -func insertHelpShortForm(shortForms []string) []string { - for _, sf := range shortForms { - if sf == "h" { - return shortForms - } - } - - return append(shortForms, "h") -} - func boolOptions(cmd Cmd) []string { f := fields(cmd.impl) b := boolFields(f) @@ -333,7 +305,18 @@ func boolOptions(cmd Cmd) []string { n = append(n, fi.Name()) } - n = insertHelpOption(n) + var hasHelp bool + for _, fi := range f { + if fi.Name() == "help" { + hasHelp = true + break + } + } + + if !hasHelp { + n = append(n, "help") + } + sfm := make(map[string][]string) for i := 0; i < len(cmd.shortForms); i += 2 { s, l := cmd.shortForms[i], cmd.shortForms[i+1] @@ -342,11 +325,20 @@ func boolOptions(cmd Cmd) []string { var sf []string for _, ni := range n { - if sn, ok := sfm[ni]; ok { - sf = append(sf, sn...) + sf = append(sf, sfm[ni]...) + } + + var hasHelpShortForm bool + for i := 0; i < len(cmd.shortForms); i += 2 { + if cmd.shortForms[i] == "h" { + hasHelpShortForm = true + break } } - sf = insertHelpShortForm(sf) + if !hasHelp && !hasHelpShortForm { + sf = append(sf, "h") + } + return append(n, sf...) } diff --git a/command_test.go b/command_test.go new file mode 100644 index 0000000..9dbee11 --- /dev/null +++ b/command_test.go @@ -0,0 +1,347 @@ +package wand + +import ( + "fmt" + "io" + "testing" +) + +func TestCommand(t *testing.T) { + t.Run("fields", func(t *testing.T) { + type s0 struct { + FooBar int + Foo struct{ Bar bool } + } + + type s1 struct { + Config string + } + + f0 := func(a s0) int { return a.FooBar } + t.Run( + "incompatible field types", + testExec(testCase{impl: f0, command: "foo"}, "duplicate fields with different types", ""), + ) + + f1 := func(a s1) string { return a.Config } + t.Run( + "config option shadowed", + testExec( + testCase{impl: f1, mergeConfTyped: []Config{ConfigFromOption()}, command: "foo"}, + "option reserved for config", + "", + ), + ) + }) + + t.Run("positional", func(t *testing.T) { + f := func(a, b, c int) int { return a + b + c } + t.Run("ok", testExec(testCase{impl: Args(Command("foo", f), 3, 3), command: "foo 1 2 3"}, "", "6")) + t.Run( + "min less than fixed", + testExec( + testCase{impl: Args(Command("foo", f), 2, 3), command: "foo 1 2 3"}, + "minimum positional", + "", + ), + ) + + t.Run( + "min more than fixed", + testExec( + testCase{impl: Args(Command("foo", f), 4, 4), command: "foo 1 2 3"}, + "minimum positional", + "", + ), + ) + + t.Run( + "max less than fixed", + testExec( + testCase{impl: Args(Command("foo", f), 0, 2), command: "foo 1 2 3"}, + "maximum positional", + "", + ), + ) + + fv := func(a, b int, c ...int) int { return a + b + len(c) } + t.Run("ok variadic", testExec(testCase{impl: Args(Command("foo", fv), 3, 5), command: "foo 1 2 3 4 5"}, "", "6")) + + t.Run( + "min less than fixed variadic", + testExec( + testCase{impl: Args(Command("foo", fv), 1, 5), command: "foo 1 2 3"}, + "minimum positional", + "", + ), + ) + + t.Run( + "max less than fixed variadic", + testExec( + testCase{impl: Args(Command("foo", fv), 0, 1), command: "foo 1 2 3"}, + "maximum positional", + "", + ), + ) + + t.Run( + "min more than max variadic", + testExec( + testCase{impl: Args(Command("foo", fv), 4, 3), command: "foo 1 2 3 4 5"}, + "minimum positional", + "", + ), + ) + + fvio := func(a int, in io.Reader, out io.Writer, b int, c ...int) int { return a + b + len(c) } + t.Run("ok io", testExec(testCase{impl: Args(Command("foo", fvio), 3, 5), command: "foo 1 2 3 4 5"}, "", "6")) + + t.Run( + "min less than fixed io", + testExec( + testCase{impl: Args(Command("foo", fvio), 1, 5), command: "foo 1 2 3"}, + "minimum positional", + "", + ), + ) + + t.Run( + "max less than fixed io", + testExec( + testCase{impl: Args(Command("foo", fvio), 0, 1), command: "foo 1 2 3"}, + "maximum positional", + "", + ), + ) + + t.Run( + "min more than max io", + testExec( + testCase{impl: Args(Command("foo", fvio), 4, 3), command: "foo 1 2 3 4 5"}, + "minimum positional", + "", + ), + ) + + fio := func(a, b io.Reader) {} + t.Run( + "max one io of a kind", + testExec(testCase{impl: fio, command: "foo"}, "only zero or one reader", ""), + ) + }) + + t.Run("impl", func(t *testing.T) { + t.Run( + "must be func", + testExec( + testCase{impl: []func(){func() {}}, command: "foo"}, + "implementation must be a function", + "", + ), + ) + + t.Run( + "paramters must be bindable", + testExec(testCase{impl: func(chan int) {}, command: "foo"}, "unsupported parameter type", ""), + ) + }) + + t.Run("command tree", func(t *testing.T) { + f := func(a, b int) int { return a + b } + cmd := Command("foo", f) + cmd = Version(cmd, "v42") + t.Run("version pass", testExec(testCase{impl: cmd, command: "foo 1 2"}, "", "3")) + cmd = Command("foo", nil) + t.Run( + "no impl and not group", + testExec(testCase{impl: cmd, command: "foo"}, "command does not have an implementation", ""), + ) + + cmd = Group("foo") + t.Run( + "no impl and no subcommands", + testExec(testCase{impl: cmd, command: "foo"}, "empty command category", ""), + ) + + cmd = Group("foo", Command("bar-baz", func() {}), Command("qux quux", func() {})) + t.Run( + "invalid subcommand name", + testExec(testCase{impl: cmd, command: "foo bar-baz"}, "command name is not a valid", ""), + ) + + cmd = Group("foo", Command("bar", func() {}), Command("bar", func() {})) + t.Run( + "duplicate subcommand name", + testExec(testCase{impl: cmd, command: "foo bar"}, "subcommand name conflict", ""), + ) + + cmd = Command("foo", func() {}, Default(Command("bar", func() {})), Command("bar", func() {})) + t.Run( + "implementation conflict", + testExec( + testCase{impl: cmd, command: "foo bar"}, + "default subcommand defined for a command that has an explicit implementation", + "", + ), + ) + + cmd = Group("foo", Default(Command("bar", func() {})), Default(Command("baz", func() {}))) + t.Run( + "multiple defaults", + testExec( + testCase{impl: cmd, command: "foo bar"}, + "multiple default subcommands", + "", + ), + ) + + cmd = Command("foo", func() {}, Command("bar", func() {}), Command("baz", func(chan int) {})) + t.Run( + "invalid subcommand", + testExec( + testCase{impl: cmd, command: "foo bar"}, + "foo:", + "", + ), + ) + }) + + t.Run("short forms", func(t *testing.T) { + type ( + s0 struct { + Foo int + Qux int + } + s1 struct{ Bar int } + s2 struct{ Baz int } + ) + + f0 := func(a s0) int { return a.Foo } + f1 := func(a s1) int { return a.Bar } + f2 := func(a s2) int { return a.Baz } + cmd := ShortForm(Command("foo", f0, ShortForm(Command("bar", f1), "a", "bar")), "a", "foo") + t.Run( + "same short form for different options", + testExec(testCase{impl: cmd, command: "foo"}, "same short form for different options", ""), + ) + + cmd = ShortForm(Command("foo", f0), "a", "foo", "a", "qux") + t.Run( + "same short form for different options on the same command", + testExec(testCase{impl: cmd, command: "foo"}, "same short form for different options", ""), + ) + + cmd = ShortForm(Command("foo", f0, Command("bar", f1)), "a", "foo", "a", "qux") + t.Run( + "same short form for different options on the same command with subcommand", + testExec(testCase{impl: cmd, command: "foo"}, "same short form for different options", ""), + ) + + cmd = Command( + "foo", + f0, + ShortForm(Command("bar", f1), "a", "bar"), + ShortForm(Command("baz", f2), "a", "baz"), + ) + + t.Run( + "same short form for different options on different branches in the tree", + testExec(testCase{impl: cmd, command: "foo"}, "same short form for different options", ""), + ) + + cmd = ShortForm(Command("foo", f0, ShortForm(Command("bar", f1), "b", "baz")), "a", "foo") + t.Run( + "unmapped short form", + testExec(testCase{impl: cmd, command: "foo"}, "unmapped short forms", ""), + ) + + cmd = ShortForm( + Command( + "foo", + f0, + ShortForm(Command("bar", f1), "b", "baz"), + ShortForm(Command("qux", f1), "b", "qux"), + ), + "a", + "foo", + ) + + t.Run( + "conflicting unmapped", + testExec( + testCase{impl: cmd, command: "foo"}, + "using the same short form for different options", + "", + ), + ) + + cmd = ShortForm(Command("foo", f0), "ab", "foo") + t.Run( + "short form not a single character", + testExec(testCase{impl: cmd, command: "foo"}, "invalid short form", ""), + ) + + cmd = ShortForm(Command("foo", f0), "6", "foo") + t.Run( + "short form not a single character", + testExec(testCase{impl: cmd, command: "foo"}, "invalid short form", ""), + ) + + cmd = ShortForm(Group("foo", Command("bar", f1)), "b", "bar") + t.Run( + "short form with command category", + testExec(testCase{impl: cmd, command: "foo bar"}, "", "0"), + ) + }) + + t.Run("bool options", func(t *testing.T) { + type s0 struct { + Foo int + Bar bool + Baz bool + } + type s1 struct { + Foo int + Bar bool + Baz bool + Help bool + } + f0 := func(a s0, b, c string) string { + return fmt.Sprint(a.Foo) + fmt.Sprint(a.Bar) + fmt.Sprint(a.Baz) + b + c + } + + f1 := func(a s1, b, c string) string { + return fmt.Sprint(a.Foo) + fmt.Sprint(a.Bar) + fmt.Sprint(a.Baz) + fmt.Sprint(a.Help) + b + c + } + + cmd := ShortForm(Command("foo", f0), "b", "bar") + t.Run( + "no help in fields and no help in short forms", + testExec(testCase{impl: cmd, command: "foo --foo 42 -b qux --baz quux"}, "", "42truetruequxquux"), + ) + + t.Run( + "no help in fields and no help in short forms help", + testExec(testCase{impl: cmd, command: "foo --help", contains: true}, "", "Synopsis"), + ) + + cmd = ShortForm(Command("foo", f0), "h", "help") + t.Run( + "help from short forms", + testExec(testCase{impl: cmd, command: "foo -h", contains: true}, "", "Synopsis"), + ) + + cmd = ShortForm(Command("foo", f1), "b", "bar") + t.Run( + "help taken by a field", + testExec(testCase{impl: cmd, command: "foo --help qux quux"}, "", "0falsefalsetruequxquux"), + ) + + cmd = ShortForm(Command("foo", f1), "h", "bar") + t.Run( + "help taken by a field and help short form used for another field", + testExec(testCase{impl: cmd, command: "foo --help qux -h quux"}, "", "0truefalsetruequxquux"), + ) + }) +} diff --git a/commandline_test.go b/commandline_test.go index e2eae1d..c0136f9 100644 --- a/commandline_test.go +++ b/commandline_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -func TestCommand(t *testing.T) { +func TestCommandLine(t *testing.T) { type f struct { One string SecondField int @@ -93,7 +93,7 @@ func TestCommand(t *testing.T) { "bool last", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three"), + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three"), command: "foo -abc true", }, "", @@ -105,7 +105,7 @@ func TestCommand(t *testing.T) { "string last", testExec( testCase{ - impl: ShortForm(Command("foo", fm), "a", "one", "b", "two", "c", "three"), + impl: ShortForm(Command("foo", fm), "a", "one", "b", "two", "c", "three"), command: "foo -abc bar", }, "", @@ -119,7 +119,7 @@ func TestCommand(t *testing.T) { "bools, short", testExec( testCase{ - impl: ShortForm(Command("foo", fl), "a", "one"), + impl: ShortForm(Command("foo", fl), "a", "one"), command: "foo -a -a -a", }, "", @@ -131,7 +131,7 @@ func TestCommand(t *testing.T) { "bools, short, combined", testExec( testCase{ - impl: ShortForm(Command("foo", fl), "a", "one"), + impl: ShortForm(Command("foo", fl), "a", "one"), command: "foo -aaa", }, "", @@ -143,7 +143,7 @@ func TestCommand(t *testing.T) { "bools, short, explicit", testExec( testCase{ - impl: ShortForm(Command("foo", fl), "a", "one"), + impl: ShortForm(Command("foo", fl), "a", "one"), command: "foo -a true -a true -a true", }, "", @@ -155,7 +155,7 @@ func TestCommand(t *testing.T) { "bools, short, combined, last explicit", testExec( testCase{ - impl: ShortForm(Command("foo", fl), "a", "one"), + impl: ShortForm(Command("foo", fl), "a", "one"), command: "foo -aaa true", }, "", @@ -167,7 +167,7 @@ func TestCommand(t *testing.T) { "bools, long", testExec( testCase{ - impl: fl, + impl: fl, command: "foo --one --one --one", }, "", @@ -179,18 +179,18 @@ func TestCommand(t *testing.T) { "bools, long, explicit", testExec( testCase{ - impl: fl, + impl: fl, command: "foo --one true --one true --one true", }, - "", - "true,true,true;"), + "", + "true,true,true;"), ) t.Run( "mixd, short", testExec( testCase{ - impl: ShortForm(Command("foo", fl), "a", "one", "b", "two"), + impl: ShortForm(Command("foo", fl), "a", "one", "b", "two"), command: "foo -a -b bar", }, "", @@ -202,7 +202,7 @@ func TestCommand(t *testing.T) { "mixed, short, combined", testExec( testCase{ - impl: ShortForm(Command("foo", fl), "a", "one", "b", "two"), + impl: ShortForm(Command("foo", fl), "a", "one", "b", "two"), command: "foo -ab bar", }, "", @@ -214,7 +214,7 @@ func TestCommand(t *testing.T) { "mixed, long", testExec( testCase{ - impl: fl, + impl: fl, command: "foo --one --two bar", }, "", @@ -226,7 +226,7 @@ func TestCommand(t *testing.T) { "mixed, long, explicit", testExec( testCase{ - impl: fl, + impl: fl, command: "foo --one true --two bar", }, "", @@ -240,7 +240,7 @@ func TestCommand(t *testing.T) { "short", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one"), + impl: ShortForm(Command("foo", fb), "a", "one"), command: "foo -a", }, "", @@ -252,7 +252,7 @@ func TestCommand(t *testing.T) { "short, multiple", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three"), + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three"), command: "foo -a -b -c", }, "", @@ -264,7 +264,7 @@ func TestCommand(t *testing.T) { "short, combined", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three"), + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three"), command: "foo -abc", }, "", @@ -276,7 +276,7 @@ func TestCommand(t *testing.T) { "short, combined, multiple", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three", "d", "four"), + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three", "d", "four"), command: "foo -ab -cd", }, "", @@ -288,7 +288,7 @@ func TestCommand(t *testing.T) { "short, multiple values", testExec( testCase{ - impl: ShortForm(Command("foo", flb), "a", "one", "b", "two", "c", "three"), + impl: ShortForm(Command("foo", flb), "a", "one", "b", "two", "c", "three"), command: "foo -aba -cab", }, "", @@ -300,7 +300,7 @@ func TestCommand(t *testing.T) { "long", testExec( testCase{ - impl: fb, + impl: fb, command: "foo --one", }, "", @@ -312,7 +312,7 @@ func TestCommand(t *testing.T) { "long, multiple", testExec( testCase{ - impl: fb, + impl: fb, command: "foo --one --two --three", }, "", @@ -324,7 +324,7 @@ func TestCommand(t *testing.T) { "long, multiple values", testExec( testCase{ - impl: flb, + impl: flb, command: "foo --one --two --one", }, "", @@ -338,7 +338,7 @@ func TestCommand(t *testing.T) { "short, true", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one"), + impl: ShortForm(Command("foo", fb), "a", "one"), command: "foo -a true", }, "", @@ -350,7 +350,7 @@ func TestCommand(t *testing.T) { "short, false", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one"), + impl: ShortForm(Command("foo", fb), "a", "one"), command: "foo -a false", }, "", @@ -362,7 +362,7 @@ func TestCommand(t *testing.T) { "short, with eq", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one"), + impl: ShortForm(Command("foo", fb), "a", "one"), command: "foo -a=true", }, "", @@ -374,7 +374,7 @@ func TestCommand(t *testing.T) { "short, true variant, capital", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a","one"), + impl: ShortForm(Command("foo", fb), "a", "one"), command: "foo -a True", }, "", @@ -386,7 +386,7 @@ func TestCommand(t *testing.T) { "short, true variant, 1", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one"), + impl: ShortForm(Command("foo", fb), "a", "one"), command: "foo -a 1", }, "", @@ -398,7 +398,7 @@ func TestCommand(t *testing.T) { "short, false variant, 0", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one"), + impl: ShortForm(Command("foo", fb), "a", "one"), command: "foo -a 0", }, "", @@ -410,7 +410,7 @@ func TestCommand(t *testing.T) { "short, combined", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one", "b", "two"), + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two"), command: "foo -ab true", }, "", @@ -422,7 +422,7 @@ func TestCommand(t *testing.T) { "short, combined, multiple", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three", "d", "four"), + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two", "c", "three", "d", "four"), command: "foo -ab true -cd true", }, "", @@ -434,7 +434,7 @@ func TestCommand(t *testing.T) { "long", testExec( testCase{ - impl: fb, + impl: fb, command: "foo --one true", }, "", @@ -446,7 +446,7 @@ func TestCommand(t *testing.T) { "long, false", testExec( testCase{ - impl: fb, + impl: fb, command: "foo --one false", }, "", @@ -458,7 +458,7 @@ func TestCommand(t *testing.T) { "logn, with eq", testExec( testCase{ - impl: fb, + impl: fb, command: "foo --one=true", }, "", @@ -469,7 +469,7 @@ func TestCommand(t *testing.T) { "long, mixed, first", testExec( testCase{ - impl: fb, + impl: fb, command: "foo --one false --two", }, "", @@ -481,7 +481,7 @@ func TestCommand(t *testing.T) { "long, mixed, last", testExec( testCase{ - impl: fb, + impl: fb, command: "foo --one --two false", }, "", @@ -495,7 +495,7 @@ func TestCommand(t *testing.T) { "short, implicit", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one"), + impl: ShortForm(Command("foo", fb), "a", "one"), command: "foo -a", }, "", @@ -506,7 +506,7 @@ func TestCommand(t *testing.T) { "short, explicit", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a", "one"), + impl: ShortForm(Command("foo", fb), "a", "one"), command: "foo -a true", }, "", @@ -518,7 +518,7 @@ func TestCommand(t *testing.T) { "short, automatic positional", testExec( testCase{ - impl: ShortForm(Command("foo", fbp), "a", "one"), + impl: ShortForm(Command("foo", fbp), "a", "one"), command: "foo -a bar", }, "", @@ -530,7 +530,7 @@ func TestCommand(t *testing.T) { "short, combined", testExec( testCase{ - impl: ShortForm(Command("foo", fb), "a","one", "b", "two"), + impl: ShortForm(Command("foo", fb), "a", "one", "b", "two"), command: "foo -ab true", }, "", @@ -542,7 +542,7 @@ func TestCommand(t *testing.T) { "short, combined, automatic positional", testExec( testCase{ - impl: ShortForm(Command("foo", fbp), "a", "one", "b", "two"), + impl: ShortForm(Command("foo", fbp), "a", "one", "b", "two"), command: "foo -ab bar", }, "", @@ -553,7 +553,7 @@ func TestCommand(t *testing.T) { "long, implicit", testExec( testCase{ - impl: fb, + impl: fb, command: "foo --one", }, "", @@ -565,7 +565,7 @@ func TestCommand(t *testing.T) { "long, explicit", testExec( testCase{ - impl: fb, + impl: fb, command: "foo --one true", }, "", @@ -577,7 +577,7 @@ func TestCommand(t *testing.T) { "long, automatic positional", testExec( testCase{ - impl: fbp, + impl: fbp, command: "foo --one bar", }, "", @@ -591,7 +591,7 @@ func TestCommand(t *testing.T) { "basic", testExec( testCase{ - impl: fp, + impl: fp, command: "foo bar baz", }, "", @@ -603,7 +603,7 @@ func TestCommand(t *testing.T) { "explicit", testExec( testCase{ - impl: fp, + impl: fp, command: "foo -- bar baz", }, "", @@ -615,7 +615,7 @@ func TestCommand(t *testing.T) { "mixed", testExec( testCase{ - impl: fp, + impl: fp, command: "foo bar -- baz", }, "", @@ -627,7 +627,7 @@ func TestCommand(t *testing.T) { "with option", testExec( testCase{ - impl: fp, + impl: fp, command: "foo bar --second-field 42 baz", }, "", @@ -639,7 +639,7 @@ func TestCommand(t *testing.T) { "with bool option at the end", testExec( testCase{ - impl: fbp, + impl: fbp, command: "foo bar baz --one", }, "", @@ -651,7 +651,7 @@ func TestCommand(t *testing.T) { "with expected bool, implicit", testExec( testCase{ - impl: fbp, + impl: fbp, command: "foo bar --one baz", }, "", @@ -663,7 +663,7 @@ func TestCommand(t *testing.T) { "with expected bool, explicit", testExec( testCase{ - impl: fbp, + impl: fbp, command: "foo bar --one true baz", }, "", @@ -675,7 +675,7 @@ func TestCommand(t *testing.T) { "option format", testExec( testCase{ - impl: fbp, + impl: fbp, command: "foo -- --one", }, "", @@ -705,7 +705,7 @@ func TestCommand(t *testing.T) { "full", testExec( testCase{ - impl: ShortForm(Command("foo", fs), "a", "foo", "b", "bar"), + impl: ShortForm(Command("foo", fs), "a", "foo", "b", "bar"), command: "foo -ab --bar baz -b --qux --quux corge -- grault", }, "", @@ -719,7 +719,7 @@ func TestCommand(t *testing.T) { "capital letters", testExec( testCase{ - impl: fp, + impl: fp, command: "foo --One bar", }, "", @@ -731,7 +731,7 @@ func TestCommand(t *testing.T) { "digit in option name", testExec( testCase{ - impl: fd, + impl: fd, command: "foo --one-2", }, "", @@ -743,7 +743,7 @@ func TestCommand(t *testing.T) { "dash in option name", testExec( testCase{ - impl: ff, + impl: ff, command: "foo --second-field 42", }, "", @@ -755,7 +755,7 @@ func TestCommand(t *testing.T) { "unpexpected character", testExec( testCase{ - impl: fp, + impl: fp, command: "foo --one#", }, "", @@ -767,7 +767,7 @@ func TestCommand(t *testing.T) { "invalid short option set", testExec( testCase{ - impl: ShortForm(Command("foo", fp), "a", "one", "b", "one", "c", "second-field"), + impl: ShortForm(Command("foo", fp), "a", "one", "b", "one", "c", "second-field"), command: "foo -aBc", }, "", @@ -779,7 +779,7 @@ func TestCommand(t *testing.T) { "positional separator, no value", testExec( testCase{ - impl: fp, + impl: fp, command: "foo --one bar --", }, "", @@ -791,7 +791,7 @@ func TestCommand(t *testing.T) { "positional separator, expecting value", testExec( testCase{ - impl: fp, + impl: fp, command: "foo --one --", }, "--one", @@ -803,7 +803,7 @@ func TestCommand(t *testing.T) { "shot flag set, expecting value", testExec( testCase{ - impl: ShortForm(Command("foo", fp), "b", "second-field"), + impl: ShortForm(Command("foo", fp), "b", "second-field"), command: "foo --one -b", }, "--one", @@ -817,7 +817,7 @@ func TestCommand(t *testing.T) { "bools", testExec( testCase{ - impl: fl, + impl: fl, command: "foo --one --one false --one", }, "", @@ -829,7 +829,7 @@ func TestCommand(t *testing.T) { "strings", testExec( testCase{ - impl: fl, + impl: fl, command: "foo --two 1 --two 2 --two 3", }, "", @@ -843,7 +843,7 @@ func TestCommand(t *testing.T) { "named", testExec( testCase{ - impl: Group("foo", Command("bar", ff), Command("baz", ff)), + impl: Group("foo", Command("bar", ff), Command("baz", ff)), command: "foo baz", }, "", @@ -855,7 +855,7 @@ func TestCommand(t *testing.T) { "default", testExec( testCase{ - impl: Group("foo", Command("bar", ff), Default(Command("baz", ff))), + impl: Group("foo", Command("bar", ff), Default(Command("baz", ff))), command: "foo", }, "", @@ -869,7 +869,7 @@ func TestCommand(t *testing.T) { "short form not defined", testExec( testCase{ - impl: fm, + impl: fm, command: "foo -h", }, "foo help", @@ -881,7 +881,7 @@ func TestCommand(t *testing.T) { "short form not help", testExec( testCase{ - impl: ShortForm(Command("foo", fm), "h", "one"), + impl: ShortForm(Command("foo", fm), "h", "one"), command: "foo -h", }, "", @@ -893,8 +893,8 @@ func TestCommand(t *testing.T) { "short form", testExec( testCase{ - impl: ShortForm(Command("foo", fm), "h", "help"), - command: "foo -h", + impl: ShortForm(Command("foo", fm), "h", "help"), + command: "foo -h", contains: true, }, "", @@ -906,8 +906,8 @@ func TestCommand(t *testing.T) { "long form", testExec( testCase{ - impl: fm, - command: "foo --help", + impl: fm, + command: "foo --help", contains: true, }, "", diff --git a/config_test.go b/config_test.go index d078570..971d75a 100644 --- a/config_test.go +++ b/config_test.go @@ -104,9 +104,9 @@ func TestConfig(t *testing.T) { "discard in previous doc", testExec( testCase{ - impl: fm, + impl: fm, mergeConf: []string{"one=bar\nsecond-var=baz", "one\nsecond-var=qux"}, - command: "foo", + command: "foo", }, "", ";qux", @@ -117,9 +117,9 @@ func TestConfig(t *testing.T) { "discard in previous same doc", testExec( testCase{ - impl: fm, + impl: fm, mergeConf: []string{"one=bar\nsecond-var=baz", "one\nsecond-var=qux\nsecond-var"}, - command: "foo", + command: "foo", }, "", ";", @@ -138,8 +138,8 @@ func TestConfig(t *testing.T) { "comments", testExec( testCase{ - impl: fm, - conf: "# comment on a line\none=bar # comment after an entry", + impl: fm, + conf: "# comment on a line\none=bar # comment after an entry", command: "foo", }, "", diff --git a/debug.go b/debug.go index a86770c..64b7aa9 100644 --- a/debug.go +++ b/debug.go @@ -11,5 +11,13 @@ func debug(a ...any) { return } + notation.Fprintln(os.Stderr, a...) +} + +func debugw(a ...any) { + if !testing.Testing() { + return + } + notation.Fprintlnw(os.Stderr, a...) } diff --git a/exec.go b/exec.go index 5584794..d1722b7 100644 --- a/exec.go +++ b/exec.go @@ -10,7 +10,6 @@ import ( ) func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, conf Config, env, args []string) { - cmd = insertHelp(cmd) _, cmd.name = filepath.Split(args[0]) if err := validateCommand(cmd, conf); err != nil { fmt.Fprintf(stderr, "program error: %v\n", err) @@ -37,6 +36,8 @@ func exec(stdin io.Reader, stdout, stderr io.Writer, exit func(int), cmd Cmd, co return } + cmd = insertHelp(cmd) + // will need root command for the config and the env: rootCmd := cmd cmd, fullCmd, args := selectCommand(cmd, args[1:]) diff --git a/exec_test.go b/exec_test.go index 0044526..a0aae87 100644 --- a/exec_test.go +++ b/exec_test.go @@ -9,13 +9,14 @@ import ( ) type testCase struct { - impl any - stdin string - conf string - mergeConf []string - env string - command string - contains bool + impl any + stdin string + conf string + mergeConfTyped []Config + mergeConf []string + env string + command string + contains bool } func testExec(test testCase, err string, expect ...string) func(*testing.T) { @@ -23,9 +24,11 @@ func testExec(test testCase, err string, expect ...string) func(*testing.T) { var exitCode int exit := func(code int) { exitCode = code } - var stdinr io.Reader - if test.stdin != "" { - stdinr = bytes.NewBuffer([]byte(test.stdin)) + var stdin io.Reader + if test.stdin == "" { + stdin = bytes.NewBuffer(nil) + } else { + stdin = bytes.NewBufferString(test.stdin) } stdout := bytes.NewBuffer(nil) @@ -33,11 +36,28 @@ func testExec(test testCase, err string, expect ...string) func(*testing.T) { cmd := wrap(test.impl) e := strings.Split(test.env, ";") a := strings.Split(test.command, " ") - - var conf Config - if test.conf != "" && len(test.mergeConf) > 0 { + + zeroOrOne := func(b ...bool) bool { + var one bool + for _, bi := range b { + if bi && one { + return false + } + + if bi { + one = true + } + } + + return true + } + + if !zeroOrOne(test.conf != "", len(test.mergeConf) > 0, len(test.mergeConfTyped) > 0) { t.Fatal("test error: conflicting test config") - } else if test.conf != "" { + } + + var conf Config + if test.conf != "" { conf = Config{test: test.conf} } else if len(test.mergeConf) > 0 { var c []Config @@ -46,9 +66,11 @@ func testExec(test testCase, err string, expect ...string) func(*testing.T) { } conf = MergeConfig(c...) + } else if len(test.mergeConfTyped) > 0 { + conf = MergeConfig(test.mergeConfTyped...) } - exec(stdinr, stdout, stderr, exit, cmd, conf, e, a) + exec(stdin, stdout, stderr, exit, cmd, conf, e, a) if exitCode != 0 && err == "" { t.Fatal("non-zero exit code:", stderr.String()) } diff --git a/go.mod b/go.mod index 50c922a..160aa87 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module code.squareroundforest.org/arpio/wand go 1.25.0 require ( - code.squareroundforest.org/arpio/bind v0.0.0-20250903234821-f3e17035cd36 + code.squareroundforest.org/arpio/bind v0.0.0-20250905213330-4591a086be1e code.squareroundforest.org/arpio/docreflect v0.0.0-20250904132730-afd27063724e code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 code.squareroundforest.org/arpio/treerack v0.0.0-20250820014405-1d956dcc6610 diff --git a/go.sum b/go.sum index 07509ef..f21ea0d 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,5 @@ -code.squareroundforest.org/arpio/bind v0.0.0-20250901011104-bcadfd8b71fc h1:nu5YXVLDrRzN9Ea5agXmhxFILyVAPyoED25ksTYC9ws= -code.squareroundforest.org/arpio/bind v0.0.0-20250901011104-bcadfd8b71fc/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= -code.squareroundforest.org/arpio/bind v0.0.0-20250903223305-8683d8ba4074 h1:OTzn0dMou+6m2rw70g7fIylQLHUTu75noAX3lbCYMqw= -code.squareroundforest.org/arpio/bind v0.0.0-20250903223305-8683d8ba4074/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= -code.squareroundforest.org/arpio/bind v0.0.0-20250903234821-f3e17035cd36 h1:8TB3ABJVV0eEdnWl+dJ3Hg4lGe+BlgNPgcW5p9yZnrQ= -code.squareroundforest.org/arpio/bind v0.0.0-20250903234821-f3e17035cd36/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= -code.squareroundforest.org/arpio/docreflect v0.0.0-20250831183400-d26ecc663a30 h1:QUCgxUEA5/ng7GwRnzb/WezmFQXSHXl48GdLJc0KC5k= -code.squareroundforest.org/arpio/docreflect v0.0.0-20250831183400-d26ecc663a30/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA= +code.squareroundforest.org/arpio/bind v0.0.0-20250905213330-4591a086be1e h1:DkOYkD12OWMAczreQESVQF7b1KsyBQq4G700oGxNy08= +code.squareroundforest.org/arpio/bind v0.0.0-20250905213330-4591a086be1e/go.mod h1:tTCmCwFABKNm3PO0Dclsp4zWhNQFTfg9+uSrgoarZFI= code.squareroundforest.org/arpio/docreflect v0.0.0-20250904132730-afd27063724e h1:f7wtGAmuTYH/VTn92sBTtKhs463q+DTtW2yKgst2kl8= code.squareroundforest.org/arpio/docreflect v0.0.0-20250904132730-afd27063724e/go.mod h1:/3xQI36oJG8qLBxT2fSS61P5/+i1T64fTX9GHRh8XhA= code.squareroundforest.org/arpio/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I= diff --git a/iniparser.gen.go b/iniparser.gen.go index 7777863..27815f2 100644 --- a/iniparser.gen.go +++ b/iniparser.gen.go @@ -1,4 +1,3 @@ - /* This file was generated with treerack (https://code.squareroundforest.org/arpio/treerack). @@ -17,30 +16,29 @@ that the user of treerack generating this file declares for it, or it is unlicensed. */ - package wand // head import ( - "strconv" - "io" - "strings" - "unicode" - "fmt" "bufio" "errors" + "fmt" + "io" + "strconv" + "strings" + "unicode" ) type charParser struct { - name string - id int - not bool - chars []rune - ranges [][]rune + name string + id int + not bool + chars []rune + ranges [][]rune } type charBuilder struct { - name string - id int + name string + id int } func (p *charParser) nodeName() string { @@ -90,22 +88,22 @@ func (b *charBuilder) build(c *context) ([]*node, bool) { } type sequenceParser struct { - name string - id int - commit commitType - items []parser - ranges [][]int - generalizations []int - allChars bool + name string + id int + commit commitType + items []parser + ranges [][]int + generalizations []int + allChars bool } type sequenceBuilder struct { - name string - id int - commit commitType - items []builder - ranges [][]int - generalizations []int - allChars bool + name string + id int + commit commitType + items []builder + ranges [][]int + generalizations []int + allChars bool } func (p *sequenceParser) nodeName() string { @@ -126,8 +124,8 @@ func (p *sequenceParser) parse(c *context) { c.results.markPending(c.offset, p.id) } var ( - currentCount int - parsed bool + currentCount int + parsed bool ) itemIndex := 0 from := c.offset @@ -229,9 +227,9 @@ func (b *sequenceBuilder) build(c *context) ([]*node, bool) { } } var ( - itemIndex int - currentCount int - nodes []*node + itemIndex int + currentCount int + nodes []*node ) for itemIndex < len(b.items) { itemFrom := c.offset @@ -271,18 +269,18 @@ func (b *sequenceBuilder) build(c *context) ([]*node, bool) { } type choiceParser struct { - name string - id int - commit commitType - options []parser - generalizations []int + name string + id int + commit commitType + options []parser + generalizations []int } type choiceBuilder struct { - name string - id int - commit commitType - options []builder - generalizations []int + name string + id int + commit commitType + options []builder + generalizations []int } func (p *choiceParser) nodeName() string { @@ -304,10 +302,10 @@ func (p *choiceParser) parse(c *context) { } c.results.markPending(c.offset, p.id) var ( - match bool - optionIndex int - foundMatch bool - failingParser parser + match bool + optionIndex int + foundMatch bool + failingParser parser ) from := c.offset to := c.offset @@ -456,9 +454,9 @@ func (s *idSet) has(id int) bool { } type results struct { - noMatch []*idSet - match [][]int - isPending [][]int + noMatch []*idSet + match [][]int + isPending [][]int } func ensureOffsetInts(ints [][]int, offset int) [][]int { @@ -597,19 +595,19 @@ func (r *results) unmarkPending(offset, id int) { } type context struct { - reader io.RuneReader - keywords []parser - offset int - readOffset int - consumed int - offsetLimit int - failOffset int - failingParser parser - readErr error - eof bool - results *results - tokens []rune - matchLast bool + reader io.RuneReader + keywords []parser + offset int + readOffset int + consumed int + offsetLimit int + failOffset int + failingParser parser + readErr error + eof bool + results *results + tokens []rune + matchLast bool } func newContext(r io.RuneReader, keywords []parser) *context { @@ -731,10 +729,10 @@ func (c *context) finalizeParse(root parser) error { } type node struct { - Name string - Nodes []*node - From, To int - tokens []rune + Name string + Nodes []*node + From, To int + tokens []rune } func (n *node) Tokens() []rune { @@ -750,8 +748,8 @@ func (n *node) Text() string { type commitType int const ( - none commitType = 0 - alias commitType = 1 << iota + none commitType = 0 + alias commitType = 1 << iota whitespace noWhitespace keyword @@ -764,17 +762,17 @@ const ( type formatFlags int const ( - formatNone formatFlags = 0 - formatPretty formatFlags = 1 << iota + formatNone formatFlags = 0 + formatPretty formatFlags = 1 << iota formatIncludeComments ) type parseError struct { - Input string - Offset int - Line int - Column int - Definition string + Input string + Offset int + Line int + Column int + Definition string } type parser interface { nodeName() string @@ -815,9 +813,214 @@ func parseInput(r io.Reader, p parser, b builder, kw []parser) (*node, error) { func parse(r io.Reader) (*node, error) { -var p60 = sequenceParser{id: 60, commit: 128,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var p58 = choiceParser{id: 58, commit: 2,};var p57 = sequenceParser{id: 57, commit: 262,name: "whitespace",allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{58,},};var p1 = charParser{id: 1,chars: []rune{32,8,12,13,9,11,},};p57.items = []parser{&p1,};p58.options = []parser{&p57,};var p59 = sequenceParser{id: 59, commit: 258,name: "doc:wsroot",ranges: [][]int{{0, 1},{0, -1},{1, 1},{0, 1},{0, 1},},};var p52 = sequenceParser{id: 52, commit: 2,ranges: [][]int{{1, 1},{0, -1},},};var p42 = sequenceParser{id: 42, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p41 = charParser{id: 41,chars: []rune{10,},};p42.items = []parser{&p41,};var p51 = sequenceParser{id: 51, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};p51.items = []parser{&p58,&p42,};p52.items = []parser{&p42,&p51,};var p43 = choiceParser{id: 43, commit: 2,};var p40 = sequenceParser{id: 40, commit: 256,name: "key-val",ranges: [][]int{{1, 1},{0, -1},{0, 1},{0, -1},{0, 1},},generalizations: []int{43,46,},};var p13 = sequenceParser{id: 13, commit: 264,name: "key",ranges: [][]int{{1, 1},{0, -1},{1, 1},{0, -1},},};var p10 = sequenceParser{id: 10, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p9 = charParser{id: 9,chars: []rune{95,},ranges: [][]rune{{97, 122},{65, 90},},};p10.items = []parser{&p9,};var p12 = sequenceParser{id: 12, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p11 = charParser{id: 11,chars: []rune{95,45,},ranges: [][]rune{{97, 122},{65, 90},{48, 57},},};p12.items = []parser{&p11,};p13.items = []parser{&p10,&p12,};var p39 = sequenceParser{id: 39, commit: 2,ranges: [][]int{{1, 1},{0, -1},{1, 1},},};var p38 = sequenceParser{id: 38, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p37 = charParser{id: 37,chars: []rune{61,},};p38.items = []parser{&p37,};var p36 = choiceParser{id: 36, commit: 256,name: "value",};var p35 = sequenceParser{id: 35, commit: 264,name: "value-chars",ranges: [][]int{{0, -1},{0, -1},},generalizations: []int{36,},};var p34 = choiceParser{id: 34, commit: 10,};var p28 = sequenceParser{id: 28, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{34,},};var p27 = charParser{id: 27,not: true,chars: []rune{92,34,10,61,35,32,8,12,13,9,11,},};p28.items = []parser{&p27,};var p33 = sequenceParser{id: 33, commit: 10,ranges: [][]int{{1, 1},{1, 1},{1, 1},{1, 1},},generalizations: []int{34,},};var p30 = sequenceParser{id: 30, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p29 = charParser{id: 29,chars: []rune{92,},};p30.items = []parser{&p29,};var p32 = sequenceParser{id: 32, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p31 = charParser{id: 31,not: true,};p32.items = []parser{&p31,};p33.items = []parser{&p30,&p32,};p34.options = []parser{&p28,&p33,};p35.items = []parser{&p34,};var p26 = sequenceParser{id: 26, commit: 264,name: "quoted",ranges: [][]int{{1, 1},{0, -1},{1, 1},{1, 1},{0, -1},{1, 1},},generalizations: []int{36,},};var p15 = sequenceParser{id: 15, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p14 = charParser{id: 14,chars: []rune{34,},};p15.items = []parser{&p14,};var p23 = choiceParser{id: 23, commit: 10,};var p17 = sequenceParser{id: 17, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{23,},};var p16 = charParser{id: 16,not: true,chars: []rune{92,34,},};p17.items = []parser{&p16,};var p22 = sequenceParser{id: 22, commit: 10,ranges: [][]int{{1, 1},{1, 1},{1, 1},{1, 1},},generalizations: []int{23,},};var p19 = sequenceParser{id: 19, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p18 = charParser{id: 18,chars: []rune{92,},};p19.items = []parser{&p18,};var p21 = sequenceParser{id: 21, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p20 = charParser{id: 20,not: true,};p21.items = []parser{&p20,};p22.items = []parser{&p19,&p21,};p23.options = []parser{&p17,&p22,};var p25 = sequenceParser{id: 25, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p24 = charParser{id: 24,chars: []rune{34,},};p25.items = []parser{&p24,};p26.items = []parser{&p15,&p23,&p25,};p36.options = []parser{&p35,&p26,};p39.items = []parser{&p38,&p58,&p36,};var p8 = sequenceParser{id: 8, commit: 258,name: "comment-line",ranges: [][]int{{1, 1},{0, 1},},generalizations: []int{43,46,},};var p3 = sequenceParser{id: 3, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p2 = charParser{id: 2,chars: []rune{35,},};p3.items = []parser{&p2,};var p7 = sequenceParser{id: 7, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var p5 = sequenceParser{id: 5, commit: 2,allChars: true,ranges: [][]int{{1, 1},},};var p4 = charParser{id: 4,not: true,chars: []rune{10,},};p5.items = []parser{&p4,};var p6 = sequenceParser{id: 6, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};p6.items = []parser{&p58,&p5,};p7.items = []parser{&p58,&p5,&p6,};p8.items = []parser{&p3,&p7,};p40.items = []parser{&p13,&p58,&p39,&p58,&p8,};p43.options = []parser{&p40,&p8,};var p54 = sequenceParser{id: 54, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var p48 = sequenceParser{id: 48, commit: 2,ranges: [][]int{{1, 1},{0, -1},{0, -1},{1, 1},},};var p45 = sequenceParser{id: 45, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p44 = charParser{id: 44,chars: []rune{10,},};p45.items = []parser{&p44,};var p47 = sequenceParser{id: 47, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};p47.items = []parser{&p58,&p45,};var p46 = choiceParser{id: 46, commit: 2,};p46.options = []parser{&p40,&p8,};p48.items = []parser{&p45,&p47,&p58,&p46,};var p53 = sequenceParser{id: 53, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};p53.items = []parser{&p58,&p48,};p54.items = []parser{&p58,&p48,&p53,};var p56 = sequenceParser{id: 56, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var p50 = sequenceParser{id: 50, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var p49 = charParser{id: 49,chars: []rune{10,},};p50.items = []parser{&p49,};var p55 = sequenceParser{id: 55, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};p55.items = []parser{&p58,&p50,};p56.items = []parser{&p58,&p50,&p55,};p59.items = []parser{&p52,&p58,&p43,&p54,&p56,};p60.items = []parser{&p58,&p59,&p58,};var b60 = sequenceBuilder{id: 60, commit: 128,name: "doc",ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var b58 = choiceBuilder{id: 58, commit: 2,};var b57 = sequenceBuilder{id: 57, commit: 262,allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{58,},};var b1 = charBuilder{};b57.items = []builder{&b1,};b58.options = []builder{&b57,};var b59 = sequenceBuilder{id: 59, commit: 258,ranges: [][]int{{0, 1},{0, -1},{1, 1},{0, 1},{0, 1},},};var b52 = sequenceBuilder{id: 52, commit: 2,ranges: [][]int{{1, 1},{0, -1},},};var b42 = sequenceBuilder{id: 42, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b41 = charBuilder{};b42.items = []builder{&b41,};var b51 = sequenceBuilder{id: 51, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};b51.items = []builder{&b58,&b42,};b52.items = []builder{&b42,&b51,};var b43 = choiceBuilder{id: 43, commit: 2,};var b40 = sequenceBuilder{id: 40, commit: 256,name: "key-val",ranges: [][]int{{1, 1},{0, -1},{0, 1},{0, -1},{0, 1},},generalizations: []int{43,46,},};var b13 = sequenceBuilder{id: 13, commit: 264,name: "key",ranges: [][]int{{1, 1},{0, -1},{1, 1},{0, -1},},};var b10 = sequenceBuilder{id: 10, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b9 = charBuilder{};b10.items = []builder{&b9,};var b12 = sequenceBuilder{id: 12, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b11 = charBuilder{};b12.items = []builder{&b11,};b13.items = []builder{&b10,&b12,};var b39 = sequenceBuilder{id: 39, commit: 2,ranges: [][]int{{1, 1},{0, -1},{1, 1},},};var b38 = sequenceBuilder{id: 38, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b37 = charBuilder{};b38.items = []builder{&b37,};var b36 = choiceBuilder{id: 36, commit: 256,name: "value",};var b35 = sequenceBuilder{id: 35, commit: 264,name: "value-chars",ranges: [][]int{{0, -1},{0, -1},},generalizations: []int{36,},};var b34 = choiceBuilder{id: 34, commit: 10,};var b28 = sequenceBuilder{id: 28, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{34,},};var b27 = charBuilder{};b28.items = []builder{&b27,};var b33 = sequenceBuilder{id: 33, commit: 10,ranges: [][]int{{1, 1},{1, 1},{1, 1},{1, 1},},generalizations: []int{34,},};var b30 = sequenceBuilder{id: 30, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b29 = charBuilder{};b30.items = []builder{&b29,};var b32 = sequenceBuilder{id: 32, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b31 = charBuilder{};b32.items = []builder{&b31,};b33.items = []builder{&b30,&b32,};b34.options = []builder{&b28,&b33,};b35.items = []builder{&b34,};var b26 = sequenceBuilder{id: 26, commit: 264,name: "quoted",ranges: [][]int{{1, 1},{0, -1},{1, 1},{1, 1},{0, -1},{1, 1},},generalizations: []int{36,},};var b15 = sequenceBuilder{id: 15, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b14 = charBuilder{};b15.items = []builder{&b14,};var b23 = choiceBuilder{id: 23, commit: 10,};var b17 = sequenceBuilder{id: 17, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},generalizations: []int{23,},};var b16 = charBuilder{};b17.items = []builder{&b16,};var b22 = sequenceBuilder{id: 22, commit: 10,ranges: [][]int{{1, 1},{1, 1},{1, 1},{1, 1},},generalizations: []int{23,},};var b19 = sequenceBuilder{id: 19, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b18 = charBuilder{};b19.items = []builder{&b18,};var b21 = sequenceBuilder{id: 21, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b20 = charBuilder{};b21.items = []builder{&b20,};b22.items = []builder{&b19,&b21,};b23.options = []builder{&b17,&b22,};var b25 = sequenceBuilder{id: 25, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b24 = charBuilder{};b25.items = []builder{&b24,};b26.items = []builder{&b15,&b23,&b25,};b36.options = []builder{&b35,&b26,};b39.items = []builder{&b38,&b58,&b36,};var b8 = sequenceBuilder{id: 8, commit: 258,ranges: [][]int{{1, 1},{0, 1},},generalizations: []int{43,46,},};var b3 = sequenceBuilder{id: 3, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b2 = charBuilder{};b3.items = []builder{&b2,};var b7 = sequenceBuilder{id: 7, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var b5 = sequenceBuilder{id: 5, commit: 2,allChars: true,ranges: [][]int{{1, 1},},};var b4 = charBuilder{};b5.items = []builder{&b4,};var b6 = sequenceBuilder{id: 6, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};b6.items = []builder{&b58,&b5,};b7.items = []builder{&b58,&b5,&b6,};b8.items = []builder{&b3,&b7,};b40.items = []builder{&b13,&b58,&b39,&b58,&b8,};b43.options = []builder{&b40,&b8,};var b54 = sequenceBuilder{id: 54, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var b48 = sequenceBuilder{id: 48, commit: 2,ranges: [][]int{{1, 1},{0, -1},{0, -1},{1, 1},},};var b45 = sequenceBuilder{id: 45, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b44 = charBuilder{};b45.items = []builder{&b44,};var b47 = sequenceBuilder{id: 47, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};b47.items = []builder{&b58,&b45,};var b46 = choiceBuilder{id: 46, commit: 2,};b46.options = []builder{&b40,&b8,};b48.items = []builder{&b45,&b47,&b58,&b46,};var b53 = sequenceBuilder{id: 53, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};b53.items = []builder{&b58,&b48,};b54.items = []builder{&b58,&b48,&b53,};var b56 = sequenceBuilder{id: 56, commit: 2,ranges: [][]int{{0, -1},{1, 1},{0, -1},},};var b50 = sequenceBuilder{id: 50, commit: 10,allChars: true,ranges: [][]int{{1, 1},{1, 1},},};var b49 = charBuilder{};b50.items = []builder{&b49,};var b55 = sequenceBuilder{id: 55, commit: 2,ranges: [][]int{{0, -1},{1, 1},},};b55.items = []builder{&b58,&b50,};b56.items = []builder{&b58,&b50,&b55,};b59.items = []builder{&b52,&b58,&b43,&b54,&b56,};b60.items = []builder{&b58,&b59,&b58,}; + var p60 = sequenceParser{id: 60, commit: 128, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} + var p58 = choiceParser{id: 58, commit: 2} + var p57 = sequenceParser{id: 57, commit: 262, name: "whitespace", allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{58}} + var p1 = charParser{id: 1, chars: []rune{32, 8, 12, 13, 9, 11}} + p57.items = []parser{&p1} + p58.options = []parser{&p57} + var p59 = sequenceParser{id: 59, commit: 258, name: "doc:wsroot", ranges: [][]int{{0, 1}, {0, -1}, {1, 1}, {0, 1}, {0, 1}}} + var p52 = sequenceParser{id: 52, commit: 2, ranges: [][]int{{1, 1}, {0, -1}}} + var p42 = sequenceParser{id: 42, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p41 = charParser{id: 41, chars: []rune{10}} + p42.items = []parser{&p41} + var p51 = sequenceParser{id: 51, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} + p51.items = []parser{&p58, &p42} + p52.items = []parser{&p42, &p51} + var p43 = choiceParser{id: 43, commit: 2} + var p40 = sequenceParser{id: 40, commit: 256, name: "key-val", ranges: [][]int{{1, 1}, {0, -1}, {0, 1}, {0, -1}, {0, 1}}, generalizations: []int{43, 46}} + var p13 = sequenceParser{id: 13, commit: 264, name: "key", ranges: [][]int{{1, 1}, {0, -1}, {1, 1}, {0, -1}}} + var p10 = sequenceParser{id: 10, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p9 = charParser{id: 9, chars: []rune{95}, ranges: [][]rune{{97, 122}, {65, 90}}} + p10.items = []parser{&p9} + var p12 = sequenceParser{id: 12, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p11 = charParser{id: 11, chars: []rune{95, 45}, ranges: [][]rune{{97, 122}, {65, 90}, {48, 57}}} + p12.items = []parser{&p11} + p13.items = []parser{&p10, &p12} + var p39 = sequenceParser{id: 39, commit: 2, ranges: [][]int{{1, 1}, {0, -1}, {1, 1}}} + var p38 = sequenceParser{id: 38, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p37 = charParser{id: 37, chars: []rune{61}} + p38.items = []parser{&p37} + var p36 = choiceParser{id: 36, commit: 256, name: "value"} + var p35 = sequenceParser{id: 35, commit: 264, name: "value-chars", ranges: [][]int{{0, -1}, {0, -1}}, generalizations: []int{36}} + var p34 = choiceParser{id: 34, commit: 10} + var p28 = sequenceParser{id: 28, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{34}} + var p27 = charParser{id: 27, not: true, chars: []rune{92, 34, 10, 61, 35, 32, 8, 12, 13, 9, 11}} + p28.items = []parser{&p27} + var p33 = sequenceParser{id: 33, commit: 10, ranges: [][]int{{1, 1}, {1, 1}, {1, 1}, {1, 1}}, generalizations: []int{34}} + var p30 = sequenceParser{id: 30, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p29 = charParser{id: 29, chars: []rune{92}} + p30.items = []parser{&p29} + var p32 = sequenceParser{id: 32, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p31 = charParser{id: 31, not: true} + p32.items = []parser{&p31} + p33.items = []parser{&p30, &p32} + p34.options = []parser{&p28, &p33} + p35.items = []parser{&p34} + var p26 = sequenceParser{id: 26, commit: 264, name: "quoted", ranges: [][]int{{1, 1}, {0, -1}, {1, 1}, {1, 1}, {0, -1}, {1, 1}}, generalizations: []int{36}} + var p15 = sequenceParser{id: 15, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p14 = charParser{id: 14, chars: []rune{34}} + p15.items = []parser{&p14} + var p23 = choiceParser{id: 23, commit: 10} + var p17 = sequenceParser{id: 17, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{23}} + var p16 = charParser{id: 16, not: true, chars: []rune{92, 34}} + p17.items = []parser{&p16} + var p22 = sequenceParser{id: 22, commit: 10, ranges: [][]int{{1, 1}, {1, 1}, {1, 1}, {1, 1}}, generalizations: []int{23}} + var p19 = sequenceParser{id: 19, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p18 = charParser{id: 18, chars: []rune{92}} + p19.items = []parser{&p18} + var p21 = sequenceParser{id: 21, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p20 = charParser{id: 20, not: true} + p21.items = []parser{&p20} + p22.items = []parser{&p19, &p21} + p23.options = []parser{&p17, &p22} + var p25 = sequenceParser{id: 25, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p24 = charParser{id: 24, chars: []rune{34}} + p25.items = []parser{&p24} + p26.items = []parser{&p15, &p23, &p25} + p36.options = []parser{&p35, &p26} + p39.items = []parser{&p38, &p58, &p36} + var p8 = sequenceParser{id: 8, commit: 258, name: "comment-line", ranges: [][]int{{1, 1}, {0, 1}}, generalizations: []int{43, 46}} + var p3 = sequenceParser{id: 3, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p2 = charParser{id: 2, chars: []rune{35}} + p3.items = []parser{&p2} + var p7 = sequenceParser{id: 7, commit: 2, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} + var p5 = sequenceParser{id: 5, commit: 2, allChars: true, ranges: [][]int{{1, 1}}} + var p4 = charParser{id: 4, not: true, chars: []rune{10}} + p5.items = []parser{&p4} + var p6 = sequenceParser{id: 6, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} + p6.items = []parser{&p58, &p5} + p7.items = []parser{&p58, &p5, &p6} + p8.items = []parser{&p3, &p7} + p40.items = []parser{&p13, &p58, &p39, &p58, &p8} + p43.options = []parser{&p40, &p8} + var p54 = sequenceParser{id: 54, commit: 2, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} + var p48 = sequenceParser{id: 48, commit: 2, ranges: [][]int{{1, 1}, {0, -1}, {0, -1}, {1, 1}}} + var p45 = sequenceParser{id: 45, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p44 = charParser{id: 44, chars: []rune{10}} + p45.items = []parser{&p44} + var p47 = sequenceParser{id: 47, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} + p47.items = []parser{&p58, &p45} + var p46 = choiceParser{id: 46, commit: 2} + p46.options = []parser{&p40, &p8} + p48.items = []parser{&p45, &p47, &p58, &p46} + var p53 = sequenceParser{id: 53, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} + p53.items = []parser{&p58, &p48} + p54.items = []parser{&p58, &p48, &p53} + var p56 = sequenceParser{id: 56, commit: 2, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} + var p50 = sequenceParser{id: 50, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var p49 = charParser{id: 49, chars: []rune{10}} + p50.items = []parser{&p49} + var p55 = sequenceParser{id: 55, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} + p55.items = []parser{&p58, &p50} + p56.items = []parser{&p58, &p50, &p55} + p59.items = []parser{&p52, &p58, &p43, &p54, &p56} + p60.items = []parser{&p58, &p59, &p58} + var b60 = sequenceBuilder{id: 60, commit: 128, name: "doc", ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} + var b58 = choiceBuilder{id: 58, commit: 2} + var b57 = sequenceBuilder{id: 57, commit: 262, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{58}} + var b1 = charBuilder{} + b57.items = []builder{&b1} + b58.options = []builder{&b57} + var b59 = sequenceBuilder{id: 59, commit: 258, ranges: [][]int{{0, 1}, {0, -1}, {1, 1}, {0, 1}, {0, 1}}} + var b52 = sequenceBuilder{id: 52, commit: 2, ranges: [][]int{{1, 1}, {0, -1}}} + var b42 = sequenceBuilder{id: 42, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b41 = charBuilder{} + b42.items = []builder{&b41} + var b51 = sequenceBuilder{id: 51, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} + b51.items = []builder{&b58, &b42} + b52.items = []builder{&b42, &b51} + var b43 = choiceBuilder{id: 43, commit: 2} + var b40 = sequenceBuilder{id: 40, commit: 256, name: "key-val", ranges: [][]int{{1, 1}, {0, -1}, {0, 1}, {0, -1}, {0, 1}}, generalizations: []int{43, 46}} + var b13 = sequenceBuilder{id: 13, commit: 264, name: "key", ranges: [][]int{{1, 1}, {0, -1}, {1, 1}, {0, -1}}} + var b10 = sequenceBuilder{id: 10, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b9 = charBuilder{} + b10.items = []builder{&b9} + var b12 = sequenceBuilder{id: 12, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b11 = charBuilder{} + b12.items = []builder{&b11} + b13.items = []builder{&b10, &b12} + var b39 = sequenceBuilder{id: 39, commit: 2, ranges: [][]int{{1, 1}, {0, -1}, {1, 1}}} + var b38 = sequenceBuilder{id: 38, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b37 = charBuilder{} + b38.items = []builder{&b37} + var b36 = choiceBuilder{id: 36, commit: 256, name: "value"} + var b35 = sequenceBuilder{id: 35, commit: 264, name: "value-chars", ranges: [][]int{{0, -1}, {0, -1}}, generalizations: []int{36}} + var b34 = choiceBuilder{id: 34, commit: 10} + var b28 = sequenceBuilder{id: 28, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{34}} + var b27 = charBuilder{} + b28.items = []builder{&b27} + var b33 = sequenceBuilder{id: 33, commit: 10, ranges: [][]int{{1, 1}, {1, 1}, {1, 1}, {1, 1}}, generalizations: []int{34}} + var b30 = sequenceBuilder{id: 30, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b29 = charBuilder{} + b30.items = []builder{&b29} + var b32 = sequenceBuilder{id: 32, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b31 = charBuilder{} + b32.items = []builder{&b31} + b33.items = []builder{&b30, &b32} + b34.options = []builder{&b28, &b33} + b35.items = []builder{&b34} + var b26 = sequenceBuilder{id: 26, commit: 264, name: "quoted", ranges: [][]int{{1, 1}, {0, -1}, {1, 1}, {1, 1}, {0, -1}, {1, 1}}, generalizations: []int{36}} + var b15 = sequenceBuilder{id: 15, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b14 = charBuilder{} + b15.items = []builder{&b14} + var b23 = choiceBuilder{id: 23, commit: 10} + var b17 = sequenceBuilder{id: 17, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}, generalizations: []int{23}} + var b16 = charBuilder{} + b17.items = []builder{&b16} + var b22 = sequenceBuilder{id: 22, commit: 10, ranges: [][]int{{1, 1}, {1, 1}, {1, 1}, {1, 1}}, generalizations: []int{23}} + var b19 = sequenceBuilder{id: 19, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b18 = charBuilder{} + b19.items = []builder{&b18} + var b21 = sequenceBuilder{id: 21, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b20 = charBuilder{} + b21.items = []builder{&b20} + b22.items = []builder{&b19, &b21} + b23.options = []builder{&b17, &b22} + var b25 = sequenceBuilder{id: 25, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b24 = charBuilder{} + b25.items = []builder{&b24} + b26.items = []builder{&b15, &b23, &b25} + b36.options = []builder{&b35, &b26} + b39.items = []builder{&b38, &b58, &b36} + var b8 = sequenceBuilder{id: 8, commit: 258, ranges: [][]int{{1, 1}, {0, 1}}, generalizations: []int{43, 46}} + var b3 = sequenceBuilder{id: 3, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b2 = charBuilder{} + b3.items = []builder{&b2} + var b7 = sequenceBuilder{id: 7, commit: 2, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} + var b5 = sequenceBuilder{id: 5, commit: 2, allChars: true, ranges: [][]int{{1, 1}}} + var b4 = charBuilder{} + b5.items = []builder{&b4} + var b6 = sequenceBuilder{id: 6, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} + b6.items = []builder{&b58, &b5} + b7.items = []builder{&b58, &b5, &b6} + b8.items = []builder{&b3, &b7} + b40.items = []builder{&b13, &b58, &b39, &b58, &b8} + b43.options = []builder{&b40, &b8} + var b54 = sequenceBuilder{id: 54, commit: 2, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} + var b48 = sequenceBuilder{id: 48, commit: 2, ranges: [][]int{{1, 1}, {0, -1}, {0, -1}, {1, 1}}} + var b45 = sequenceBuilder{id: 45, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b44 = charBuilder{} + b45.items = []builder{&b44} + var b47 = sequenceBuilder{id: 47, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} + b47.items = []builder{&b58, &b45} + var b46 = choiceBuilder{id: 46, commit: 2} + b46.options = []builder{&b40, &b8} + b48.items = []builder{&b45, &b47, &b58, &b46} + var b53 = sequenceBuilder{id: 53, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} + b53.items = []builder{&b58, &b48} + b54.items = []builder{&b58, &b48, &b53} + var b56 = sequenceBuilder{id: 56, commit: 2, ranges: [][]int{{0, -1}, {1, 1}, {0, -1}}} + var b50 = sequenceBuilder{id: 50, commit: 10, allChars: true, ranges: [][]int{{1, 1}, {1, 1}}} + var b49 = charBuilder{} + b50.items = []builder{&b49} + var b55 = sequenceBuilder{id: 55, commit: 2, ranges: [][]int{{0, -1}, {1, 1}}} + b55.items = []builder{&b58, &b50} + b56.items = []builder{&b58, &b50, &b55} + b59.items = []builder{&b52, &b58, &b43, &b54, &b56} + b60.items = []builder{&b58, &b59, &b58} -var keywords = []parser{} + var keywords = []parser{} -return parseInput(r, &p60, &b60, keywords) + return parseInput(r, &p60, &b60, keywords) } diff --git a/notes.txt b/notes.txt index f5c0316..e46d872 100644 --- a/notes.txt +++ b/notes.txt @@ -1,3 +1,6 @@ +use a type cache +support unix timestamps in bind and in reflect +verify that duration can be parsed from integer strings test: - nil return values - options in variadic diff --git a/reflect.go b/reflect.go index 28fdbf0..6f96e05 100644 --- a/reflect.go +++ b/reflect.go @@ -145,10 +145,30 @@ func compatibleTypes(t ...bind.FieldType) bool { } switch t[0] { - case bind.Any: + case bind.String, bind.Any: return compatibleTypes(t[1:]...) + } + + switch t[1] { + case bind.String, bind.Any: + return compatibleTypes(t[1:]...) + } + + switch t[0] { + case bind.Bool: + switch t[1] { + case bind.Bool: + return compatibleTypes(t[1:]...) + default: + return false + } + } + + switch t[1] { + case bind.Bool: + return false default: - return t[0] == t[1] && compatibleTypes(t[1:]...) + return compatibleTypes(t[1:]...) } } diff --git a/reflect_test.go b/reflect_test.go index 74916f1..de4877c 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -3,6 +3,7 @@ package wand import ( "bytes" "code.squareroundforest.org/arpio/wand/internal/tests/testlib" + "fmt" "io" "testing" "time" @@ -96,22 +97,94 @@ func TestReflect(t *testing.T) { t.Run("compatible types", func(t *testing.T) { type s0 struct { FooBar int - Foo struct{ Bar string } + Foo struct{ Bar bool } } + + type s0a struct { + FooBar bool + Foo struct{ Bar int } + } + type s1 struct { FooBar int Foo struct{ Bar int } } - f := func(a s0) int { return a.FooBar + len(a.Foo.Bar) } + + f := func(a s0) string { return fmt.Sprint(a.FooBar) + fmt.Sprint(a.Foo.Bar) } + fa := func(a s0a) string { return fmt.Sprint(a.FooBar) + fmt.Sprint(a.Foo.Bar) } g := func(a s1) int { return a.FooBar + a.Foo.Bar } t.Run("incompatible", testExec(testCase{impl: f, command: "foo --foo-bar 42"}, "duplicate fields with different types", "")) + t.Run("incompatible", testExec(testCase{impl: fa, command: "foo --foo-bar 42"}, "duplicate fields with different types", "")) t.Run("compatible", testExec(testCase{impl: g, command: "foo --foo-bar 42"}, "", "84")) + type s2 struct { FooBar any Foo struct{ Bar int } } + + type s2a struct { + FooBar int + Foo struct{ Bar any } + } + h := func(a s2) int { return len(a.FooBar.(string)) + a.Foo.Bar } t.Run("any interface", testExec(testCase{impl: h, command: "foo --foo-bar 42"}, "", "44")) + ha := func(a s2a) int { return a.FooBar + len(a.Foo.Bar.(string)) } + t.Run("any interface", testExec(testCase{impl: ha, command: "foo --foo-bar 42"}, "", "44")) + + type s3 struct { + FooBar string + Foo struct{ Bar int } + } + + i := func(a s2) string { return fmt.Sprint(a.FooBar) + fmt.Sprint(a.Foo.Bar) } + t.Run("string", testExec(testCase{impl: i, command: "foo --foo-bar 42"}, "", "4242")) + + type s4 struct { + FooBar bool + Foo struct{ Bar bool } + } + + j := func(a s4) string { return fmt.Sprint(a.FooBar) + fmt.Sprint(a.Foo.Bar) } + t.Run("bool", testExec(testCase{impl: j, command: "foo --foo-bar"}, "", "truetrue")) + + type s5 struct { + FooBar time.Duration + Foo struct{ Bar time.Duration } + } + + k := func(a s5) string { return fmt.Sprint(a.FooBar) + fmt.Sprint(a.Foo.Bar) } + t.Run("duration and duration", testExec(testCase{impl: k, command: "foo --foo-bar 9s"}, "", "9s9s")) + + type s6 struct { + FooBar time.Duration + Foo struct{ Bar int } + } + + l := func(a s6) string { return fmt.Sprint(a.FooBar) + fmt.Sprint(a.Foo.Bar) } + t.Run( + "duration and numeric", + testExec(testCase{impl: l, command: "foo --foo-bar 9000000000"}, "", "9s9000000000"), + ) + + type s7 struct { + FooBar time.Time + Foo struct{ Bar time.Time } + } + + m := func(a s7) string { return fmt.Sprint(a.FooBar.Unix()) + fmt.Sprint(a.Foo.Bar.Unix()) } + t.Run("time and time", testExec(testCase{impl: m, command: "foo --foo-bar 1757097011"}, "", "17570970111757097011")) + + type s8 struct { + FooBar time.Time + Foo struct{ Bar int } + } + + n := func(a s8) string { return fmt.Sprint(a.FooBar.Unix()) + fmt.Sprint(a.Foo.Bar) } + t.Run( + "time and numeric", + testExec(testCase{impl: n, command: "foo --foo-bar 1757097011"}, "", "17570970111757097011"), + ) }) t.Run("bind failure", func(t *testing.T) {