diff --git a/command.go b/command.go index ba0724b..c60160e 100644 --- a/command.go +++ b/command.go @@ -19,19 +19,21 @@ func wrap(impl any) Cmd { return Command("", impl) } -func validateFields(f []bind.Field, conf Config) error { +func validateFields(f map[string][]bind.Field, conf Config) error { hasConfigFromOption := hasConfigFromOption(conf) - mf := make(map[string]bind.Field) - for _, fi := range f { - if ef, ok := mf[fi.Name()]; ok && !compatibleTypes(fi.Type(), ef.Type()) { - return fmt.Errorf("duplicate fields with different types: %s", fi.Name()) + for name, ff := range f { + var t []bind.FieldType + for _, fi := range ff { + t = append(t, fi.Type()) } - if hasConfigFromOption && fi.Name() == "config" { + if !compatibleTypes(t...) { + return fmt.Errorf("duplicate fields with different types: %s", name) + } + + if hasConfigFromOption && name == "config" { return errors.New("option reserved for config file shadowed by struct field") } - - mf[fi.Name()] = fi } return nil @@ -98,7 +100,7 @@ func validateImpl(cmd Cmd, conf Config) error { } } - f := fields(cmd.impl) + f := mapFields(cmd.impl) if err := validateFields(f, conf); err != nil { return err } diff --git a/docreflect.gen.go b/docreflect.gen.go index c003166..45379f1 100644 --- a/docreflect.gen.go +++ b/docreflect.gen.go @@ -2,31 +2,30 @@ Generated with https://code.squareroundforest.org/arpio/docreflect */ + package wand - import "code.squareroundforest.org/arpio/docreflect" - func init() { - docreflect.Register("code.squareroundforest.org/arpio/wand/tools", "") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\nfunc(out, packageName, gopaths)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Exec", "\nfunc(o, stdin, args)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions", "") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.CacheDir", "") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.Import", "") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.InlineImport", "") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, commandDir)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, o, commandDir)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execTransparent", "\nfunc(command, args)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, args)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports, inlineImports)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports, inlineImports)") - docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin)") -} +docreflect.Register("code.squareroundforest.org/arpio/wand/tools", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Docreflect", "\nfunc(out, packageName, gopaths)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Exec", "\nfunc(o, stdin, args)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.CacheDir", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.ClearCache", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.Import", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.InlineImport", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.ExecOptions.NoCache", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Man", "\nfunc(out, commandDir)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.Markdown", "\nfunc(out, o, commandDir)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.MarkdownOptions.Level", "") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.commandReader", "\nfunc(in)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execCommandDir", "\nfunc(out, commandDir, env)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execInternal", "\nfunc(command, args)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execTransparent", "\nfunc(command, args)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execWand", "\nfunc(o, args)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.execc", "\nfunc(stdin, stdout, stderr, command, args, env)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.hash", "\nfunc(expression, imports, inlineImports)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.printGoFile", "\nfunc(fn, expression, imports, inlineImports)") +docreflect.Register("code.squareroundforest.org/arpio/wand/tools.readExec", "\nfunc(o, stdin)") +} \ No newline at end of file diff --git a/docs.go b/docs.go deleted file mode 100644 index 88393ae..0000000 --- a/docs.go +++ /dev/null @@ -1,5 +0,0 @@ -/* -Wand provides utilities for constructing command line applications from functions, with automatic parameter -binding from command line arguments, environment variables and configuration files. -*/ -package wand diff --git a/go.mod b/go.mod index f42ca04..57a8455 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-20250901011104-bcadfd8b71fc + code.squareroundforest.org/arpio/bind v0.0.0-20250903223305-8683d8ba4074 code.squareroundforest.org/arpio/docreflect v0.0.0-20250831183400-d26ecc663a30 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 b206c9f..6d506cf 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ 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/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/notation v0.0.0-20250826181910-5140794b16b2 h1:S4mjQHL70CuzFg1AGkr0o0d+4M+ZWM0sbnlYq6f0b3I= diff --git a/lib.go b/lib.go index 0ef000c..5b7f08d 100644 --- a/lib.go +++ b/lib.go @@ -1,3 +1,5 @@ +// Wand provides utilities for constructing command line applications from functions, with automatic parameter +// binding from command line arguments, environment variables and configuration files. package wand import ( diff --git a/notes.txt b/notes.txt index 84b3f05..f5c0316 100644 --- a/notes.txt +++ b/notes.txt @@ -1,4 +1,3 @@ -review if any symbols unused test: - nil return values - options in variadic @@ -11,3 +10,6 @@ test: - implementation in pointer - implementation in slice => not accepted - implementation in pointer and slice => not accepted +- plurality +- env parsing +- plurality in input validation diff --git a/reflect.go b/reflect.go index e6c6787..8d3efdc 100644 --- a/reflect.go +++ b/reflect.go @@ -49,13 +49,13 @@ func or[T any](p ...func(T) bool) func(T) bool { } } -func unpackTypeChecked(visited map[reflect.Type]bool, t reflect.Type) reflect.Type { +func circularChecked(visited map[reflect.Type]bool, t reflect.Type) bool { if t == nil { - return t + return false } if visited[t] { - return t + return true } if visited == nil { @@ -65,14 +65,40 @@ func unpackTypeChecked(visited map[reflect.Type]bool, t reflect.Type) reflect.Ty visited[t] = true switch t.Kind() { case reflect.Pointer, reflect.Slice: - return unpackTypeChecked(visited, t.Elem()) + return circularChecked(visited, t.Elem()) + case reflect.Struct: + for i := 0; i < t.NumField(); i++ { + svisited := make(map[reflect.Type]bool) + for t := range visited { + svisited[t] = true + } + + if circularChecked(svisited, t.Field(i).Type) { + return true + } + } + + return false default: - return t + return false } } +func circular(t reflect.Type) bool { + return circularChecked(nil, t) +} + func unpackType(t reflect.Type) reflect.Type { - return unpackTypeChecked(nil, t) + if t == nil { + return nil + } + + switch t.Kind() { + case reflect.Pointer, reflect.Slice: + return unpackType(t.Elem()) + default: + return t + } } func unpackValueChecked(visited map[uintptr]bool, v reflect.Value) reflect.Value { @@ -119,6 +145,7 @@ func isTime(t reflect.Type) bool { return false } + t = unpackType(t) return t.ConvertibleTo(reflect.TypeFor[time.Time]()) } @@ -147,7 +174,7 @@ func isWriter(t reflect.Type) bool { return t.NumMethod() == 1 && t.Implements(reflect.TypeFor[io.Writer]()) } -func compatibleTypes(t ...bind.Scalar) bool { +func compatibleTypes(t ...bind.FieldType) bool { if len(t) == 0 { return false } @@ -186,8 +213,7 @@ func structParameters(f any) []reflect.Type { func structFields(s reflect.Type) []bind.Field { s = unpackType(s) - v := allocate(reflect.PointerTo(s)) - return bind.FieldValues(v.Interface()) + return bind.FieldsOf(s) } func fields(f any) []bind.Field { @@ -260,6 +286,10 @@ func bindable(t reflect.Type) bool { return false } + if circular(t) { + return false + } + t = unpackType(t) if isTime(t) { return true @@ -294,7 +324,7 @@ func bindable(t reflect.Type) bool { } } -func scalarTypeString(t bind.Scalar) string { +func scalarTypeString(t bind.FieldType) string { r := reflect.TypeOf(t) p := strings.Split(r.Name(), ".") n := p[len(p)-1] @@ -302,30 +332,30 @@ func scalarTypeString(t bind.Scalar) string { return n } -func canScan(t bind.Scalar, v any) bool { +func canScan(t bind.FieldType, v any) bool { switch t { case bind.Any: return true case bind.Bool: - _, ok := bind.BindScalarCreate[bool](v) + _, ok := bind.CreateAndBindScalar[bool](v) return ok - case bind.Int: - _, ok := bind.BindScalarCreate[int](v) + case bind.Int, bind.Int8, bind.Int16, bind.Int32, bind.Int64: + _, ok := bind.CreateAndBindScalar[int](v) return ok - case bind.Uint: - _, ok := bind.BindScalarCreate[uint](v) + case bind.Uint, bind.Uint8, bind.Uint16, bind.Uint32, bind.Uint64: + _, ok := bind.CreateAndBindScalar[uint](v) return ok - case bind.Float: - _, ok := bind.BindScalarCreate[float64](v) + case bind.Float32, bind.Float64: + _, ok := bind.CreateAndBindScalar[float64](v) return ok case bind.String: - _, ok := bind.BindScalarCreate[string](v) + _, ok := bind.CreateAndBindScalar[string](v) return ok case bind.Duration: - _, ok := bind.BindScalarCreate[time.Duration](v) + _, ok := bind.CreateAndBindScalar[time.Duration](v) return ok case bind.Time: - _, ok := bind.BindScalarCreate[time.Time](v) + _, ok := bind.CreateAndBindScalar[time.Time](v) return ok default: return false @@ -372,7 +402,7 @@ func bindFields(receiver reflect.Value, values map[string][]any) []string { } } - unmapped := bind.BindFields(receiver.Interface(), f...) + unmapped := bind.Bind(receiver.Interface(), f...) var names []string for _, um := range unmapped { diff --git a/reflect_test.go b/reflect_test.go index 5690822..163611a 100644 --- a/reflect_test.go +++ b/reflect_test.go @@ -7,15 +7,21 @@ import ( ) func TestReflect(t *testing.T) { - t.Run("pack and unpack", func(t *testing.T) { - f := func(a int) int { return a } - t.Run("no need to pack", testExec(testCase{impl: f, command: "foo 42"}, "", "42")) - g := func(a *int) int { return *a } - t.Run("pointer", testExec(testCase{impl: g, command: "foo 42"}, "", "42")) - h := func(a []int) int { return a[0] } - t.Run("slice", testExec(testCase{impl: h, command: "foo 42"}, "", "42")) - i := func(a *[]*[]int) int { return (*((*a)[0]))[0] } - t.Run("pointer and slice", testExec(testCase{impl: i, command: "foo 42"}, "", "42")) + t.Run("unpack", func(t *testing.T) { + f := func(a []int) int { return a[0] } + t.Run("slice", testExec(testCase{impl: f, command: "foo 42"}, "", "42")) + ps := func(a *[]*[]int) int { return (*((*a)[0]))[0] } + t.Run("pointer and slice", testExec(testCase{impl: ps, command: "foo 42"}, "", "42")) + type s struct{Foo int; Bar *s} + c := func(v s) int { return v.Foo } + t.Run("circular type", testExec(testCase{impl: c, command: "foo --foo 42"}, "unsupported parameter type", "")) + fp := func(a int) int { return a } + t.Run("function pointer", testExec(testCase{impl: &fp, command: "foo 42"}, "", "42")) + var p any + p = &p + t.Run("circular reference", testExec(testCase{impl: p, command: "foo"}, "must be a function or a pointer to a function", "")) + var i any + t.Run("nil interface", testExec(testCase{impl: &i, command: "foo"}, "must be a function or a pointer to a function", "")) }) t.Run("io params", func(t *testing.T) { @@ -81,4 +87,16 @@ func TestReflect(t *testing.T) { t.Run("unscannable", testExec(testCase{impl: g, command: "foo bar"}, "unsupported parameter type", "")) }) }) + + t.Run("compatible types", func(t *testing.T) { + type s0 struct{FooBar int; Foo struct {Bar string}} + type s1 struct{FooBar int; Foo struct {Bar int}} + f := func(a s0) int { return a.FooBar + len(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("compatible", testExec(testCase{impl: g, command: "foo --foo-bar 42"}, "", "84")) + type s2 struct{FooBar any; Foo struct {Bar int}} + 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")) + }) }