From 88bc6f6ab7154694101a304dd5f5582feb122463 Mon Sep 17 00:00:00 2001 From: Arpad Ryszka Date: Wed, 27 Aug 2025 21:03:25 +0200 Subject: [PATCH] bind scalar --- .gitignore | 1 + Makefile | 27 +++++ go.mod | 3 + lib.go | 68 ++++++++++++ scalar.go | 78 ++++++++++++++ scalar_test.go | 206 ++++++++++++++++++++++++++++++++++++ scan.go | 82 +++++++++++++++ scan_test.go | 120 +++++++++++++++++++++ struct.go | 1 + type.go | 168 ++++++++++++++++++++++++++++++ type_test.go | 278 +++++++++++++++++++++++++++++++++++++++++++++++++ 11 files changed, 1032 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 go.mod create mode 100644 lib.go create mode 100644 scalar.go create mode 100644 scalar_test.go create mode 100644 scan.go create mode 100644 scan_test.go create mode 100644 struct.go create mode 100644 type.go create mode 100644 type_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ebf0f2e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.cover diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ae35e0b --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +SOURCES = $(shell find . -name "*.go") + +default: build + +lib: $(SOURCES) + go build + +build: lib + +check: $(SOURCES) build + go test -count 1 + +.cover: $(SOURCES) build + go test -count 1 -coverprofile .cover + +cover: .cover + go tool cover -func .cover + +showcover: .cover + go tool cover -html .cover + +fmt: $(SOURCES) + go fmt + +clean: + go clean + rm -f .cover diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..e7d23b1 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module code.squareroundforest.org/arpio/bind + +go 1.25.0 diff --git a/lib.go b/lib.go new file mode 100644 index 0000000..878aa22 --- /dev/null +++ b/lib.go @@ -0,0 +1,68 @@ +// provides more flexible and permissive ways of setting values than reflect.Value.Set +package bind + +type Field struct { + name string + path []string + list bool + value any +} + +// the receiver must be addressable +func BindScalar(receiver any, value ...any) bool { + return bindScalarReflect(receiver, value) +} + +func BindScalarCreate[T any](value ...any) (T, bool) { + return bindScalarCreateReflect[T](value) +} + +func FieldValue(path []string, value any) Field { + return Field{path: path, value: value} +} + +func FieldValueByName(name string, value any) Field { + return Field{name: name, value: value} +} + +func (f Field) Path() []string { + p := make([]string, len(f.path)) + copy(p, f.path) + return p +} + +func (f Field) Name() string { return f.name } +func (f Field) List() bool { return f.list } +func (f Field) Value() any { return f.value } + +func Fields[T any]() []Field { + return nil +} + +func FieldValues(structure any) []Field { + return nil +} + +func BindFields(structure any, values []Field) []Field { + return nil +} + +func BindFieldsCreate[T any](values []Field) (any, []Field) { + return nil, nil +} + +func AcceptsScalar[T any]() bool { + return acceptsScalarReflect[T]() +} + +func AcceptsFields[T any]() bool { + return acceptsFieldsReflect[T]() +} + +func AcceptsList[T any]() bool { + return acceptsListReflect[T]() +} + +func Bindable[T any]() bool { + return bindableReflect[T]() +} diff --git a/scalar.go b/scalar.go new file mode 100644 index 0000000..9942e06 --- /dev/null +++ b/scalar.go @@ -0,0 +1,78 @@ +package bind + +import "reflect" + +func bindScalar(receiver reflect.Value, values []any) bool { + if len(values) == 0 { + return false + } + + if !acceptsScalar(receiver.Type()) { + return false + } + + if len(values) == 1 { + receiver = unpackValue(receiver, pointer|iface|slice) + r := reflect.ValueOf(values[0]) + r = unpackValue(r, pointer|iface|slice) + v, ok := scan(receiver.Type(), r.Interface()) + if !ok { + return false + } + + if !receiver.CanSet() { + return false + } + + receiver.Set(reflect.ValueOf(v)) + return true + } + + receiver = unpackValue(receiver, pointer|iface|anytype) + if receiver.Kind() != reflect.Slice || receiver.Len() < len(values) { + return false + } + + for i := range values { + if !bindScalar(receiver.Index(i), []any{values[i]}) { + return false + } + } + + return true +} + +func bindScalarCreate(t reflect.Type, values []any) (reflect.Value, bool) { + if len(values) == 0 { + return reflect.Zero(t), false + } + + if !acceptsScalar(t) { + return reflect.Zero(t), false + } + + receiver, ok := allocate(t, len(values)) + if !ok { + return reflect.Zero(t), false + } + + if !bindScalar(receiver, values) { + return reflect.Zero(t), false + } + + return receiver, true +} + +func bindScalarReflect(receiver any, values []any) bool { + return bindScalar(reflect.ValueOf(receiver), values) +} + +func bindScalarCreateReflect[T any](values []any) (T, bool) { + v, ok := bindScalarCreate(reflect.TypeFor[T](), values) + if !ok { + var v T + return v, false + } + + return v.Interface().(T), true +} diff --git a/scalar_test.go b/scalar_test.go new file mode 100644 index 0000000..dc41ef6 --- /dev/null +++ b/scalar_test.go @@ -0,0 +1,206 @@ +package bind_test + +import ( + "code.squareroundforest.org/arpio/bind" + "slices" + "testing" +) + +type valuer interface { + value() int +} + +type counter interface { + count() int +} + +type countedSlice []int + +type valueInt int + +func (s countedSlice) count() int { + return len(s) +} + +func (i *valueInt) value() int { + return int(*i) +} + +func TestScalar(t *testing.T) { + t.Run("bind scalar", func(t *testing.T) { + t.Run("no value", func(t *testing.T) { + var i int + if bind.BindScalar(&i) { + t.Fatal() + } + }) + + t.Run("not scalar", func(t *testing.T) { + var s struct{ Foo int } + if bind.BindScalar(&s, "42") { + t.Fatal() + } + }) + + t.Run("cannot scan", func(t *testing.T) { + var i int + if bind.BindScalar(i, "foo") { + t.Fatal() + } + }) + + t.Run("receiver cannot be set", func(t *testing.T) { + var i int + if bind.BindScalar(i, "42") { + t.Fatal() + } + }) + + t.Run("one value set", func(t *testing.T) { + var i int + if !bind.BindScalar(&i, "42") || i != 42 { + t.Fatal(i) + } + }) + + t.Run("multiple values, not slice", func(t *testing.T) { + var i int + if bind.BindScalar(&i, "21", "42", "84") { + t.Fatal() + } + }) + + t.Run("multiple values, small slice", func(t *testing.T) { + s := make([]int, 2) + if bind.BindScalar(&s, "21", "42", "84") { + t.Fatal() + } + }) + + t.Run("multiple values set", func(t *testing.T) { + s := make([]int, 3) + if !bind.BindScalar(&s, "21", "42", "84") || !slices.Equal(s, []int{21, 42, 84}) { + t.Fatal() + } + }) + + t.Run("multiple values set, larger slice", func(t *testing.T) { + s := make([]int, 4) + if !bind.BindScalar(&s, "21", "42", "84") || !slices.Equal(s, []int{21, 42, 84, 0}) { + t.Fatal() + } + }) + + t.Run("multiple values, cannot scan", func(t *testing.T) { + s := make([]int, 3) + if bind.BindScalar(&s, "21", "42", "foo") { + t.Fatal() + } + }) + + t.Run("set item of a slice", func(t *testing.T) { + s := []int{21, 42, 84} + if !bind.BindScalar(&s, "27") || !slices.Equal(s, []int{27, 42, 84}) { + t.Fatal(s) + } + }) + + t.Run("interface receiver", func(t *testing.T) { + var i any + if !bind.BindScalar(&i, 42) || i != 42 { + t.Fatal(i) + } + }) + + t.Run("interface receiver has value", func(t *testing.T) { + var ( + i int + iface any + ) + + i = 21 + iface = &i + if ok := bind.BindScalar(&iface, "42"); !ok || iface != "42" || i != 21 { + t.Fatal(ok, iface) + } + }) + + t.Run("wrapped with non-empty interface", func(t *testing.T) { + // this could work in theory, but fails on checking the type of the receiver + // we leave the test undefiend, and checking only for no panic + var iface valuer + i := valueInt(21) + p := &i + iface = p + ok := bind.BindScalar(&iface, "42") + if ok != (i == 42) { + t.Fatal() + } + }) + + t.Run("slice wrapped by empty interface", func(t *testing.T) { + var iface any + s := make([]int, 3) + iface = s + if ok := bind.BindScalar(&iface, "21", "42", "84"); !ok || !slices.Equal(s, []int{21, 42, 84}) { + t.Fatal() + } + }) + }) + + t.Run("bind scalar with create", func(t *testing.T) { + t.Run("no value", func(t *testing.T) { + if _, ok := bind.BindScalarCreate[int](); ok { + t.Fatal() + } + }) + + t.Run("does not accept scalar", func(t *testing.T) { + if _, ok := bind.BindScalarCreate[struct{Foo int}]("42"); ok { + t.Fatal() + } + }) + + t.Run("empty interface", func(t *testing.T) { + if v, ok := bind.BindScalarCreate[any]("42"); !ok || v != "42" { + t.Fatal() + } + }) + + t.Run("scalar", func(t *testing.T) { + if v, ok := bind.BindScalarCreate[int]("42"); !ok || v != 42 { + t.Fatal() + } + }) + + t.Run("slice", func(t *testing.T) { + if v, ok := bind.BindScalarCreate[[]int]("21", "42", "84"); !ok || !slices.Equal(v, []int{21, 42, 84}) { + t.Fatal() + } + }) + + t.Run("slice with non-scalar type", func(t *testing.T) { + if _, ok := bind.BindScalarCreate[[]func(int)]("21", "42", "84"); ok { + t.Fatal() + } + }) + + t.Run("pointer to non-scalar", func(t *testing.T) { + if _, ok := bind.BindScalarCreate[*func()]("42"); ok { + t.Fatal() + } + }) + + t.Run("pointer", func(t *testing.T) { + if v, ok := bind.BindScalarCreate[*int]("42"); !ok || *v != 42 { + t.Fatal() + } + }) + + t.Run("unscannable", func(t *testing.T) { + if _, ok := bind.BindScalarCreate[int]("foo"); ok { + t.Fatal() + } + }) + }) +} diff --git a/scan.go b/scan.go new file mode 100644 index 0000000..8f4e679 --- /dev/null +++ b/scan.go @@ -0,0 +1,82 @@ +package bind + +import ( + "fmt" + "reflect" + "strconv" + "strings" +) + +func intParse[T any](parse func(string, int, int) (T, error), s string, byteSize int) (T, error) { + bitSize := byteSize * 8 + switch { + case strings.HasPrefix(s, "0b"): + return parse(s[2:], 2, bitSize) + case strings.HasPrefix(s, "0x"): + return parse(s[2:], 16, bitSize) + case strings.HasPrefix(s, "0"): + return parse(s[1:], 8, bitSize) + default: + return parse(s, 10, bitSize) + } +} + +func parseInt(s string, byteSize int) (int64, error) { + return intParse(strconv.ParseInt, s, byteSize) +} + +func parseUint(s string, byteSize int) (uint64, error) { + return intParse(strconv.ParseUint, s, byteSize) +} + +func scanConvert(t reflect.Type, v any) (any, bool) { + r := reflect.ValueOf(v) + if !r.CanConvert(t) { + return nil, false + } + + return r.Convert(t).Interface(), true +} + +func scanString(t reflect.Type, s string) (any, bool) { + var ( + v any + err error + ) + + switch t.Kind() { + case reflect.Bool: + v, err = strconv.ParseBool(s) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + v, err = parseInt(s, int(t.Size())) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + v, err = parseUint(s, int(t.Size())) + case reflect.Float32, reflect.Float64: + v, err = strconv.ParseFloat(s, int(t.Size())*8) + case reflect.String: + v = s + default: + return nil, false + } + + if err != nil { + return nil, false + } + + p := reflect.New(t) + p.Elem().Set(reflect.ValueOf(v).Convert(t)) + return p.Elem().Interface(), true +} + +func scan(t reflect.Type, v any) (any, bool) { + if vv, ok := scanConvert(t, v); ok { + return vv, true + } + + r := reflect.ValueOf(v) + if r.Kind() != reflect.String { + return nil, false + } + + return scanString(t, fmt.Sprint(v)) +} diff --git a/scan_test.go b/scan_test.go new file mode 100644 index 0000000..db0cdc0 --- /dev/null +++ b/scan_test.go @@ -0,0 +1,120 @@ +package bind_test + +import ( + "testing" + "code.squareroundforest.org/arpio/bind" +) + +func TestScan(t *testing.T) { + // conversion + // unscannable + // bool + // int + // int sized + // uint + // uint sized + // binary + // octal + // hexa + // num parse failing + + t.Run("conversion", func(t *testing.T) { + var i64 int64 + i := 42 + bind.BindScalar(&i64, i) + if i64 != 42 { + t.Fatal() + } + }) + + t.Run("unscannable", func(t *testing.T) { + var s string + if bind.BindScalar(&s, func() string { return "" }) { + t.Fatal() + } + }) + + t.Run("bool", func(t *testing.T) { + var b bool + if !bind.BindScalar(&b, "true") || !b { + t.Fatal() + } + }) + + t.Run("int", func(t *testing.T) { + var i int + if !bind.BindScalar(&i, "42") || i != 42 { + t.Fatal() + } + }) + + t.Run("int sized", func(t *testing.T) { + var i int8 + if !bind.BindScalar(&i, "42") || i != 42 { + t.Fatal() + } + }) + + t.Run("uint", func(t *testing.T) { + var i uint + if !bind.BindScalar(&i, "42") || i != 42 { + t.Fatal() + } + }) + + t.Run("uint sized", func(t *testing.T) { + var i uint8 + if !bind.BindScalar(&i, "42") || i != 42 { + t.Fatal() + } + }) + + t.Run("binary", func(t *testing.T) { + var i int + if !bind.BindScalar(&i, "0b101010") || i != 42 { + t.Fatal() + } + }) + + t.Run("octal", func(t *testing.T) { + var i int + if !bind.BindScalar(&i, "052") || i != 42 { + t.Fatal() + } + }) + + t.Run("hexa", func(t *testing.T) { + var i int + if !bind.BindScalar(&i, "0x2a") || i != 42 { + t.Fatal() + } + }) + + t.Run("num parse failing", func(t *testing.T) { + i := 21 + if bind.BindScalar(&i, "foo") || i != 21 { + t.Fatal() + } + }) + + t.Run("float", func(t *testing.T) { + var f float64 + if !bind.BindScalar(&f, "2.41") || f != 2.41 { + t.Fatal() + } + }) + + t.Run("string", func(t *testing.T) { + var s string + if !bind.BindScalar(&s, "foo bar baz") || s != "foo bar baz" { + t.Fatal() + } + }) + + t.Run("interface", func(t *testing.T) { + var i any + if !bind.BindScalar(&i, "foo bar baz") || i != "foo bar baz" { + t.Fatal() + } + }) +} diff --git a/struct.go b/struct.go new file mode 100644 index 0000000..92a8c5c --- /dev/null +++ b/struct.go @@ -0,0 +1 @@ +package bind diff --git a/type.go b/type.go new file mode 100644 index 0000000..36f2344 --- /dev/null +++ b/type.go @@ -0,0 +1,168 @@ +package bind + +import "reflect" + +type unpackFlag int + +const ( + pointer unpackFlag = 1 << iota + slice + anytype + iface +) + +func (f unpackFlag) has(v unpackFlag) bool { + return f&v > 0 +} + +func isAny(t reflect.Type) bool { + return t.Kind() == reflect.Interface && t.NumMethod() == 0 +} + +func isInterface(t reflect.Type) bool { + return t.Kind() == reflect.Interface && t.NumMethod() > 0 +} + +func isScalar(t reflect.Type) bool { + switch t.Kind() { + case reflect.Bool, + reflect.Int, + reflect.Int8, + reflect.Int16, + reflect.Int32, + reflect.Int64, + reflect.Uint, + reflect.Uint8, + reflect.Uint16, + reflect.Uint32, + reflect.Uint64, + reflect.Uintptr, + reflect.Float32, + reflect.Float64, + reflect.String: + return true + default: + return false + } +} + +func isScalarMap(t reflect.Type) bool { + if t.Kind() != reflect.Map { + return false + } + + key := unpackType(t.Key(), pointer) + if key.Kind() != reflect.String { + return false + } + + value := unpackType(t.Elem(), pointer|slice) + return isAny(value) || isScalar(value) +} + +func unpackType(t reflect.Type, unpack unpackFlag) reflect.Type { + if unpack.has(pointer) && t.Kind() == reflect.Pointer { + return unpackType(t.Elem(), unpack) + } + + if unpack.has(slice) && t.Kind() == reflect.Slice { + return unpackType(t.Elem(), unpack) + } + + return t +} + +func unpackValue(v reflect.Value, unpack unpackFlag) reflect.Value { + if unpack.has(pointer) && v.Kind() == reflect.Pointer { + return unpackValue(v.Elem(), unpack) + } + + if unpack.has(slice) && v.Kind() == reflect.Slice && v.Len() > 0 { + return unpackValue(v.Index(0), unpack) + } + + if unpack.has(anytype) && isAny(v.Type()) && !v.IsNil() { + return unpackValue(v.Elem(), unpack) + } + + if unpack.has(iface) && isInterface(v.Type()) && !v.IsNil() { + return unpackValue(v.Elem(), unpack) + } + + return v +} + +func acceptsScalar(t reflect.Type) bool { + t = unpackType(t, pointer|slice) + return isAny(t) || isScalar(t) +} + +func acceptsFields(t reflect.Type) bool { + t = unpackType(t, pointer|slice) + return t.Kind() == reflect.Struct || isScalarMap(t) +} + +func acceptsList(t reflect.Type) bool { + if !bindable(t) { + return false + } + + t = unpackType(t, pointer) + return t.Kind() == reflect.Slice +} + +func bindable(t reflect.Type) bool { + return acceptsScalar(t) || acceptsFields(t) +} + +func acceptsScalarReflect[T any]() bool { + return acceptsScalar(reflect.TypeFor[T]()) +} + +func acceptsFieldsReflect[T any]() bool { + return acceptsFields(reflect.TypeFor[T]()) +} + +func acceptsListReflect[T any]() bool { + return acceptsList(reflect.TypeFor[T]()) +} + +func bindableReflect[T any]() bool { + return bindable(reflect.TypeFor[T]()) +} + +// expected to be called with types that can pass the bindable check +func allocate(t reflect.Type, len int) (reflect.Value, bool) { + if len == 0 { + return reflect.Zero(t), false + } + + if isAny(t) || isScalar(t) || t.Kind() == reflect.Struct || isScalarMap(t) { + p := reflect.New(t) + return p.Elem(), len == 1 + } + + if t.Kind() == reflect.Slice { + l := reflect.MakeSlice(t, len, len) + for i := 0; i < len; i++ { + e, ok := allocate(t.Elem(), 1) + if !ok { + return reflect.Zero(t), false + } + + l.Index(i).Set(e) + } + + return l, true + } + + // must be pointer + e, ok := allocate(t.Elem(), len) + if !ok { + return reflect.Zero(t), false + } + + p := reflect.New(t.Elem()) + p.Elem().Set(e) + return p, true +} diff --git a/type_test.go b/type_test.go new file mode 100644 index 0000000..7fbb9e6 --- /dev/null +++ b/type_test.go @@ -0,0 +1,278 @@ +package bind_test + +import ( + "code.squareroundforest.org/arpio/bind" + "testing" +) + +func TestTypeChecks(t *testing.T) { + t.Run("accepts scalar", func(t *testing.T) { + t.Run("bool", func(t *testing.T) { + if !bind.AcceptsScalar[bool]() { + t.Fatal() + } + }) + + t.Run("int", func(t *testing.T) { + if !bind.AcceptsScalar[int]() { + t.Fatal() + } + }) + + t.Run("uint", func(t *testing.T) { + if !bind.AcceptsScalar[uint]() { + t.Fatal() + } + }) + + t.Run("float", func(t *testing.T) { + if !bind.AcceptsScalar[int]() { + t.Fatal() + } + }) + + t.Run("string", func(t *testing.T) { + if !bind.AcceptsScalar[bool]() { + t.Fatal() + } + }) + + t.Run("any", func(t *testing.T) { + if !bind.AcceptsScalar[any]() { + t.Fatal() + } + }) + + t.Run("pointer", func(t *testing.T) { + type p *int + if !bind.AcceptsScalar[p]() { + t.Fatal() + } + }) + + t.Run("slice", func(t *testing.T) { + type s []int + if !bind.AcceptsScalar[s]() { + t.Fatal() + } + }) + + t.Run("pointer and slice combined", func(t *testing.T) { + type c *[]*[]int + if !bind.AcceptsScalar[c]() { + t.Fatal() + } + }) + + t.Run("struct", func(t *testing.T) { + type s struct{ Foo int } + if bind.AcceptsScalar[s]() { + t.Fatal() + } + }) + + t.Run("map", func(t *testing.T) { + if bind.AcceptsScalar[map[string]int]() { + t.Fatal() + } + }) + + t.Run("interface with methods", func(t *testing.T) { + type i interface{ Foo(int) } + if bind.AcceptsScalar[i]() { + t.Fatal() + } + }) + + t.Run("func", func(t *testing.T) { + if bind.AcceptsScalar[func(int)]() { + t.Fatal() + } + }) + + t.Run("chan", func(t *testing.T) { + if bind.AcceptsScalar[chan int]() { + t.Fatal() + } + }) + }) + + t.Run("accepts fields", func(t *testing.T) { + t.Run("struct", func(t *testing.T) { + type s struct{ Foo int } + if !bind.AcceptsFields[s]() { + t.Fatal() + } + }) + + t.Run("map", func(t *testing.T) { + if !bind.AcceptsFields[map[string]int]() { + t.Fatal() + } + }) + + t.Run("map with interface fields", func(t *testing.T) { + if !bind.AcceptsFields[map[string]any]() { + t.Fatal() + } + }) + + t.Run("map with list fields", func(t *testing.T) { + if !bind.AcceptsFields[map[string][]int]() { + t.Fatal() + } + }) + + t.Run("pointer", func(t *testing.T) { + type p *struct{ Foo int } + if !bind.AcceptsFields[p]() { + t.Fatal() + } + }) + + t.Run("slice", func(t *testing.T) { + type s []struct{ Foo int } + if !bind.AcceptsFields[s]() { + t.Fatal() + } + }) + + t.Run("pointer and slice combined", func(t *testing.T) { + type c *[]*[]struct{ Foo int } + if !bind.AcceptsFields[c]() { + t.Fatal() + } + }) + + t.Run("wrong map key", func(t *testing.T) { + if bind.AcceptsFields[map[int]int]() { + t.Fatal() + } + }) + + t.Run("wrong map value", func(t *testing.T) { + if bind.AcceptsFields[map[string]struct{ Foo int }]() { + t.Fatal() + } + }) + + t.Run("scalar", func(t *testing.T) { + if bind.AcceptsFields[int]() { + t.Fatal() + } + }) + + t.Run("interface", func(t *testing.T) { + type i interface{ Foo(int) } + if bind.AcceptsFields[i]() { + t.Fatal() + } + }) + + t.Run("any type", func(t *testing.T) { + if bind.AcceptsFields[any]() { + t.Fatal() + } + }) + + t.Run("func", func(t *testing.T) { + if bind.AcceptsFields[func(int)]() { + t.Fatal() + } + }) + + t.Run("chan", func(t *testing.T) { + if bind.AcceptsFields[chan int]() { + t.Fatal() + } + }) + }) + + t.Run("accepts list", func(t *testing.T) { + t.Run("scalars", func(t *testing.T) { + if !bind.AcceptsList[[]int]() { + t.Fatal() + } + }) + + t.Run("structs", func(t *testing.T) { + if !bind.AcceptsList[[]struct{ Foo int }]() { + t.Fatal() + } + }) + + t.Run("maps", func(t *testing.T) { + if !bind.AcceptsList[[]map[string]int]() { + t.Fatal() + } + }) + + t.Run("pointer to list", func(t *testing.T) { + if !bind.AcceptsList[*[]int]() { + t.Fatal() + } + }) + + t.Run("scalar", func(t *testing.T) { + if bind.AcceptsList[int]() { + t.Fatal() + } + }) + + t.Run("struct", func(t *testing.T) { + if bind.AcceptsList[struct{ Foo int }]() { + t.Fatal() + } + }) + + t.Run("map", func(t *testing.T) { + if bind.AcceptsList[map[string]int]() { + t.Fatal() + } + }) + + t.Run("map", func(t *testing.T) { + if bind.AcceptsList[map[string]int]() { + t.Fatal() + } + }) + + t.Run("struct pointer", func(t *testing.T) { + if bind.AcceptsList[*struct{ Foo int }]() { + t.Fatal() + } + }) + + t.Run("wrong elem type", func(t *testing.T) { + if bind.AcceptsList[map[string]struct{ Foo int }]() { + t.Fatal() + } + }) + }) + + t.Run("bindable", func(t *testing.T) { + t.Run("scalar", func(t *testing.T) { + if !bind.Bindable[int]() { + t.Fatal() + } + }) + + t.Run("struct", func(t *testing.T) { + if !bind.Bindable[struct{ Foo int }]() { + t.Fatal() + } + }) + + t.Run("func", func(t *testing.T) { + if bind.Bindable[func()]() { + t.Fatal() + } + }) + + t.Run("chan", func(t *testing.T) { + if bind.Bindable[chan int]() { + t.Fatal() + } + }) + }) +}