bind scalar
This commit is contained in:
commit
88bc6f6ab7
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.cover
|
||||||
27
Makefile
Normal file
27
Makefile
Normal file
@ -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
|
||||||
3
go.mod
Normal file
3
go.mod
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
module code.squareroundforest.org/arpio/bind
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
68
lib.go
Normal file
68
lib.go
Normal file
@ -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]()
|
||||||
|
}
|
||||||
78
scalar.go
Normal file
78
scalar.go
Normal file
@ -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
|
||||||
|
}
|
||||||
206
scalar_test.go
Normal file
206
scalar_test.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
82
scan.go
Normal file
82
scan.go
Normal file
@ -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))
|
||||||
|
}
|
||||||
120
scan_test.go
Normal file
120
scan_test.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
168
type.go
Normal file
168
type.go
Normal file
@ -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
|
||||||
|
}
|
||||||
278
type_test.go
Normal file
278
type_test.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user