1
0

bind scalar

This commit is contained in:
Arpad Ryszka 2025-08-27 21:03:25 +02:00
commit 88bc6f6ab7
11 changed files with 1032 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.cover

27
Makefile Normal file
View 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
View File

@ -0,0 +1,3 @@
module code.squareroundforest.org/arpio/bind
go 1.25.0

68
lib.go Normal file
View 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
View 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
View 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
View 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
View 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()
}
})
}

1
struct.go Normal file
View File

@ -0,0 +1 @@
package bind

168
type.go Normal file
View 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
View 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()
}
})
})
}