update bind dependency version

This commit is contained in:
Arpad Ryszka 2025-09-04 01:09:24 +02:00
parent 221de5c509
commit c05cfc7656
9 changed files with 122 additions and 72 deletions

View File

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

View File

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

View File

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

2
go.mod
View File

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

2
go.sum
View File

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

2
lib.go
View File

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

View File

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

View File

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

View File

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