1
0
bind/field_test.go
2025-08-31 17:11:09 +02:00

788 lines
18 KiB
Go

package bind_test
import (
"code.squareroundforest.org/arpio/bind"
"code.squareroundforest.org/arpio/notation"
"slices"
"sort"
"strings"
"testing"
)
func TestField(t *testing.T) {
sortFields := func(f []bind.Field, p ...func(int, int) bool) {
sort.Slice(f, func(i, j int) bool {
pi, pj := strings.Join(f[i].Path(), ":"), strings.Join(f[j].Path(), ":")
if pi < pj {
return true
}
if f[i].Name() < f[j].Name() {
return true
}
for _, pi := range p {
if pi(i, j) {
return true
}
}
return false
})
}
t.Run("fields", func(t *testing.T) {
type s0 struct {
FieldOne int
}
type s1 struct {
Foo int
}
type s2 struct {
s0
foo int
FooBar int
Baz s1
Qux *s1
Chan chan int
Que bool
Hola []int
}
f := bind.Fields[s2]()
if len(f) != 6 {
t.Fatal(notation.Sprintwt(f))
}
m := make(map[string]bind.Field)
for _, fi := range f {
m[fi.Name()] = fi
}
fieldOne := m["field-one"]
if !slices.Equal(fieldOne.Path(), []string{"FieldOne"}) {
t.Fatal(fieldOne.Name())
}
fooBar := m["foo-bar"]
if !slices.Equal(fooBar.Path(), []string{"FooBar"}) {
t.Fatal(fooBar.Name())
}
bazFoo := m["baz-foo"]
if !slices.Equal(bazFoo.Path(), []string{"Baz", "Foo"}) {
t.Fatal(bazFoo.Name())
}
quxFoo := m["qux-foo"]
if !slices.Equal(quxFoo.Path(), []string{"Qux", "Foo"}) {
t.Fatal(quxFoo.Name())
}
que := m["que"]
if !slices.Equal(que.Path(), []string{"Que"}) || que.Type() != bind.Bool {
t.Fatal(que.Name())
}
hola := m["hola"]
if !slices.Equal(hola.Path(), []string{"Hola"}) || !hola.List() {
t.Fatal(hola.Name())
}
t.Run("cannot have fields", func(t *testing.T) {
type i []int
f := bind.Fields[i]()
if len(f) != 0 {
t.Fatal()
}
})
t.Run("has circular type", func(t *testing.T) {
type s struct{ Foo *s }
if len(bind.Fields[s]()) != 0 {
t.Fatal()
}
})
t.Run("list", func(t *testing.T) {
type s struct{ Foo int }
f := bind.Fields[[]s]()
if len(f) != 1 || !f[0].List() {
t.Fatal()
}
})
})
t.Run("field values", func(t *testing.T) {
t.Run("has circular reference", func(t *testing.T) {
type s struct{ Foo any }
var v s
v.Foo = v
if len(bind.FieldValues(v)) != 0 {
t.Fatal()
}
})
t.Run("slice", func(t *testing.T) {
type s struct{ Foo int }
f := bind.FieldValues([]s{{21}, {42}})
if len(f) != 2 || f[0].Value() != 21 || f[1].Value() != 42 {
t.Fatal()
}
})
t.Run("scalar map", func(t *testing.T) {
f := bind.FieldValues(map[string]int{"foo": 21, "bar": 42})
sortFields(f)
if len(f) != 2 ||
f[0].Name() != "bar" || f[0].Value() != 42 || !f[0].Free() ||
f[1].Name() != "foo" || f[1].Value() != 21 || !f[1].Free() {
t.Fatal(notation.Sprint(f))
}
})
t.Run("scalar map with list values", func(t *testing.T) {
f := bind.FieldValues(map[string][]int{"foo": []int{21, 36}, "bar": []int{42, 72}})
sortFields(f)
if len(f) != 4 ||
f[0].Name() != "bar" || f[0].Value() != 42 || !f[0].Free() ||
f[1].Name() != "bar" || f[1].Value() != 72 || !f[1].Free() ||
f[2].Name() != "foo" || f[2].Value() != 21 || !f[2].Free() ||
f[3].Name() != "foo" || f[3].Value() != 36 || !f[3].Free() {
t.Fatal(notation.Sprint(f))
}
})
t.Run("not a struct", func(t *testing.T) {
v := []int{21, 42, 84}
f := bind.FieldValues(v)
if len(f) != 3 || f[0].Value() != 21 || f[1].Value() != 42 || f[2].Value() != 84 {
t.Fatal()
}
})
t.Run("not exported field", func(t *testing.T) {
type s struct {
Foo int
bar string
}
v := s{Foo: 42, bar: "baz"}
f := bind.FieldValues(v)
if len(f) != 1 || f[0].Name() != "foo" {
t.Fatal(notation.Sprint(f))
}
})
t.Run("scalar fields", func(t *testing.T) {
type s struct {
Foo int
Bar bool
}
v := s{Foo: 42, Bar: true}
f := bind.FieldValues(v)
if len(f) != 2 ||
f[0].Name() != "foo" || !slices.Equal(f[0].Path(), []string{"Foo"}) || f[0].Value() != 42 || f[0].Type() == bind.Bool ||
f[1].Name() != "bar" || !slices.Equal(f[1].Path(), []string{"Bar"}) || f[1].Value() != true || f[1.].Type() != bind.Bool {
t.Fatal()
}
})
t.Run("list field", func(t *testing.T) {
type s struct{ Foo []int }
v := s{Foo: []int{21, 42, 84}}
f := bind.FieldValues(v)
if len(f) != 3 ||
f[0].Name() != "foo" || f[0].Value() != 21 ||
f[1].Name() != "foo" || f[1].Value() != 42 ||
f[2].Name() != "foo" || f[2].Value() != 84 {
t.Fatal(notation.Sprintwt(f))
}
})
t.Run("list of lists", func(t *testing.T) {
type s struct{ Foo [][]int }
v := s{Foo: [][]int{{21}, {42}}}
f := bind.FieldValues(v)
if len(f) != 2 ||
f[0].Name() != "foo" || f[0].Value() != 21 ||
f[1].Name() != "foo" || f[1].Value() != 42 {
t.Fatal()
}
})
t.Run("list of scalar maps", func(t *testing.T) {
type s struct{ Foo any }
v := s{Foo: []any{map[string]int{"foo": 42}}}
f := bind.FieldValues(v)
if len(f) != 1 || f[0].Name() != "foo-foo" || f[0].Value() != 42 {
t.Fatal(notation.Sprintwt(f))
}
})
t.Run("list of structs", func(t *testing.T) {
type s struct{ Foo any }
v := s{Foo: []any{s{Foo: 21}, s{Foo: 42}}}
f := bind.FieldValues(v)
if len(f) != 2 ||
f[0].Name() != "foo-foo" || f[0].Value() != 21 ||
f[1].Name() != "foo-foo" || f[1].Value() != 42 {
t.Fatal()
}
})
t.Run("scalar map field", func(t *testing.T) {
type s struct{ Foo map[string]int }
v := s{Foo: map[string]int{"foo": 42}}
f := bind.FieldValues(v)
if len(f) != 1 || f[0].Name() != "foo-foo" || f[0].Value() != 42 {
t.Fatal()
}
})
t.Run("anonymous field", func(t *testing.T) {
type s0 struct{ Foo int }
type s1 struct {
s0
Bar int
}
var v s1
v.Foo = 21
v.Bar = 42
f := bind.FieldValues(v)
if len(f) != 2 ||
f[0].Name() != "foo" || f[0].Value() != 21 ||
f[1].Name() != "bar" || f[1].Value() != 42 {
t.Fatal()
}
})
t.Run("child struct", func(t *testing.T) {
type S0 struct{ Foo int }
type s1 struct {
S0 S0
Bar int
}
var v s1
v.S0.Foo = 21
v.Bar = 42
f := bind.FieldValues(v)
if len(f) != 2 ||
f[0].Name() != "s-0-foo" || f[0].Value() != 21 ||
f[1].Name() != "bar" || f[1].Value() != 42 {
t.Fatal(notation.Sprintwt(f))
}
})
})
t.Run("bind fields", func(t *testing.T) {
t.Run("no circular receiver", func(t *testing.T) {
type s struct {
Foo *s
Bar int
}
var v s
if len(bind.BindFields(&v, bind.NamedValue("bar", 42))) != 1 {
t.Fatal()
}
})
t.Run("no circular valeu", func(t *testing.T) {
type s struct{ Foo int }
type p *p
var v p
if len(bind.BindFields(&v, bind.NamedValue("foo", 42))) != 1 {
t.Fatal()
}
})
t.Run("set by name", func(t *testing.T) {
type s struct{ FooBar int }
var v s
u := bind.BindFields(&v, bind.NamedValue("foo-bar", 42))
if len(u) != 0 || v.FooBar != 42 {
t.Fatal(notation.Sprint(u), notation.Sprint(v))
}
})
t.Run("set by path", func(t *testing.T) {
type s struct{ FooBar int }
var v s
u := bind.BindFields(&v, bind.ValueByPath([]string{"FooBar"}, 42))
if len(u) != 0 || v.FooBar != 42 {
t.Fatal(notation.Sprint(u), notation.Sprint(v))
}
})
t.Run("fail to bind", func(t *testing.T) {
type s struct {
Foo int
Bar int
}
var v s
u := bind.BindFields(
&v,
bind.NamedValue("foo", 21),
bind.NamedValue("baz", 42),
)
if len(u) != 1 || u[0].Name() != "baz" {
t.Fatal()
}
})
t.Run("bind list", func(t *testing.T) {
type s struct{ Foo []int }
var v s
u := bind.BindFields(
&v,
bind.NamedValue("foo", 21),
bind.NamedValue("foo", 42),
bind.NamedValue("foo", 84),
)
if len(u) != 0 || v.Foo[0] != 21 || v.Foo[1] != 42 || v.Foo[2] != 84 {
t.Fatal(notation.Sprint(u), notation.Sprint(v))
}
})
t.Run("bind list of structs", func(t *testing.T) {
type s struct{ Foo []struct{ Bar int } }
var v s
u := bind.BindFields(
&v,
bind.NamedValue("foo-bar", 21),
bind.NamedValue("foo-bar", 42),
bind.NamedValue("foo-bar", 84),
)
if len(u) != 0 || len(v.Foo) != 3 || v.Foo[0].Bar != 21 || v.Foo[1].Bar != 42 || v.Foo[2].Bar != 84 {
t.Fatal()
}
})
t.Run("bind list in list", func(t *testing.T) {
type s struct{ Foo []struct{ Bar []int } }
var v s
u := bind.BindFields(
&v,
bind.NamedValue("foo-bar", 21),
bind.NamedValue("foo-bar", 42),
bind.NamedValue("foo-bar", 84),
)
if len(u) != 0 || len(v.Foo) != 3 ||
len(v.Foo[0].Bar) != 1 || v.Foo[0].Bar[0] != 21 ||
len(v.Foo[1].Bar) != 1 || v.Foo[1].Bar[0] != 42 ||
len(v.Foo[2].Bar) != 1 || v.Foo[2].Bar[0] != 84 {
t.Fatal()
}
})
t.Run("list receiver", func(t *testing.T) {
var l []struct{ Foo int }
u := bind.BindFields(
&l,
bind.NamedValue("foo", 21),
bind.NamedValue("foo", 42),
bind.NamedValue("foo", 84),
)
if len(u) != 0 || len(l) != 3 || l[0].Foo != 21 || l[1].Foo != 42 || l[2].Foo != 84 {
t.Fatal()
}
})
t.Run("list short and cannot be set", func(t *testing.T) {
type (
s0 struct{ Bar int }
s1 struct{ Foo []s0 }
)
v := s1{[]s0{{1}, {2}}}
u := bind.BindFields(
v,
bind.NamedValue("foo-bar", 21),
bind.NamedValue("foo-bar", 42),
bind.NamedValue("foo-bar", 84),
)
if len(u) != 3 || len(v.Foo) != 2 || v.Foo[0].Bar != 1 || v.Foo[1].Bar != 2 {
t.Fatal(notation.Sprint(u), notation.Sprint(v))
}
})
t.Run("list short and gets reset", func(t *testing.T) {
type (
s0 struct{ Bar int }
s1 struct{ Foo []s0 }
)
v := s1{[]s0{{1}, {2}}}
u := bind.BindFields(
&v,
bind.NamedValue("foo-bar", 21),
bind.NamedValue("foo-bar", 42),
bind.NamedValue("foo-bar", 84),
)
if len(u) != 0 || len(v.Foo) != 3 || v.Foo[0].Bar != 21 || v.Foo[1].Bar != 42 || v.Foo[2].Bar != 84 {
t.Fatal(notation.Sprint(u), notation.Sprint(v))
}
})
t.Run("list has invalid type", func(t *testing.T) {
type (
s0 struct{ Bar chan int }
s1 struct{ Foo []s0 }
)
v := s1{[]s0{{nil}, {nil}}}
u := bind.BindFields(
&v,
bind.NamedValue("foo-bar", 21),
bind.NamedValue("foo-bar", 42),
bind.NamedValue("foo-bar", 84),
)
if len(u) != 3 {
t.Fatal(notation.Sprint(u), notation.Sprint(v))
}
})
t.Run("bind scalar map", func(t *testing.T) {
m := make(map[string]int)
u := bind.BindFields(m, bind.NamedValue("foo-bar", 21), bind.NamedValue("baz-qux", 42))
if len(u) != 0 || len(m) != 2 || m["foo-bar"] != 21 || m["baz-qux"] != 42 {
t.Fatal()
}
})
t.Run("scalar map key conversion", func(t *testing.T) {
type key string
m := make(map[key]int)
u := bind.BindFields(m, bind.NamedValue("foo", 42))
if len(u) != 0 || len(m) != 1 || m["foo"] != 42 {
t.Fatal()
}
})
t.Run("scalar map pointer key", func(t *testing.T) {
m := make(map[*string]int)
u := bind.BindFields(m, bind.NamedValue("foo", 42))
if len(u) != 0 || len(m) != 1 {
t.Fatal()
}
for key, value := range m {
if *key != "foo" || value != 42 {
t.Fatal()
}
}
})
t.Run("scalar map list value", func(t *testing.T) {
m := make(map[string][]int)
u := bind.BindFields(m, bind.NamedValue("foo", 21), bind.NamedValue("foo", 42), bind.NamedValue("foo", 84))
if len(u) != 0 || len(m) != 1 || !slices.Equal(m["foo"], []int{21, 42, 84}) {
t.Fatal(notation.Sprint(u), notation.Sprint(m))
}
})
t.Run("scalar map pointer value", func(t *testing.T) {
m := make(map[string]*int)
u := bind.BindFields(m, bind.NamedValue("foo", 42))
if len(u) != 0 || len(m) != 1 {
t.Fatal()
}
for key, value := range m {
if key != "foo" || *value != 42 {
t.Fatal()
}
}
})
t.Run("scalar map list pointer value", func(t *testing.T) {
m := make(map[string]*[]int)
u := bind.BindFields(m, bind.NamedValue("foo", 21), bind.NamedValue("foo", 42), bind.NamedValue("foo", 84))
if len(u) != 0 || len(m) != 1 || !slices.Equal(*m["foo"], []int{21, 42, 84}) {
t.Fatal()
}
})
t.Run("allocate scalar map", func(t *testing.T) {
type s struct{ Foo map[string]int }
var v s
u := bind.BindFields(&v, bind.NamedValue("foo-bar", 42))
if len(u) != 0 || len(v.Foo) != 1 || v.Foo["bar"] != 42 {
t.Fatal()
}
})
t.Run("scalar map addressing via path", func(t *testing.T) {
type s struct{ Foo map[string]int }
var v s
u := bind.BindFields(&v, bind.ValueByPath([]string{"Foo", "Bar"}, 42))
if len(u) != 0 || len(v.Foo) != 1 || v.Foo["Bar"] != 42 {
t.Fatal()
}
})
t.Run("scalar map and too long field path", func(t *testing.T) {
m := make(map[string]int)
u := bind.BindFields(m, bind.ValueByPath([]string{"foo", "bar"}, 42))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("scalar map cannot be set", func(t *testing.T) {
type s struct{ Foo map[string]int }
var v s
u := bind.BindFields(v, bind.NamedValue("foo-bar", 42))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("scalar map with wrong value type", func(t *testing.T) {
m := make(map[string]int)
u := bind.BindFields(m, bind.NamedValue("foo", "bar"))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("struct fields", func(t *testing.T) {
type s struct {
Foo int
Bar struct{ Baz string }
}
var v s
u := bind.BindFields(
&v,
bind.NamedValue("foo", 42),
bind.NamedValue("bar-baz", "qux"),
)
if len(u) != 0 || v.Foo != 42 || v.Bar.Baz != "qux" {
t.Fatal()
}
})
t.Run("non-existing field", func(t *testing.T) {
type s struct {
Foo int
Bar struct{ Baz string }
}
var v s
u := bind.BindFields(
&v,
bind.NamedValue("foo", 42),
bind.NamedValue("bar-qux", "qux"),
)
if len(u) != 1 || u[0].Name() != "bar-qux" || v.Foo != 42 || v.Bar.Baz != "" {
t.Fatal()
}
})
t.Run("too many fields", func(t *testing.T) {
type s struct {
Foo int
Bar struct{ Baz string }
}
var v s
u := bind.BindFields(
&v,
bind.NamedValue("foo", 42),
bind.NamedValue("bar-baz", "qux"),
bind.NamedValue("bar-baz", "quux"),
)
if len(u) != 2 || u[0].Name() != "bar-baz" || u[1].Name() != "bar-baz" || v.Foo != 42 {
t.Fatal()
}
})
t.Run("pointer fields", func(t *testing.T) {
type s struct {
Foo *int
Bar *struct{ Baz *string }
Qux *[]struct{ Quux string }
}
var v s
u := bind.BindFields(
&v,
bind.NamedValue("foo", 42),
bind.NamedValue("bar-baz", "qux"),
bind.NamedValue("qux-quux", "corge"),
)
if len(u) != 0 || *v.Foo != 42 || *v.Bar.Baz != "qux" || len(*v.Qux) != 1 || (*v.Qux)[0].Quux != "corge" {
t.Fatal()
}
})
t.Run("unsupported pointer fields", func(t *testing.T) {
type s struct {
Foo *int
Bar *chan int
}
var v s
u := bind.BindFields(
&v,
bind.NamedValue("foo", 42),
bind.NamedValue("bar", "qux"),
bind.NamedValue("bar-baz", "corge"),
)
sortFields(u)
if len(u) != 2 || u[0].Name() != "bar" || u[1].Name() != "bar-baz" || *v.Foo != 42 {
t.Fatal(notation.Sprint(u), notation.Sprint(v))
}
})
t.Run("struct fields by path", func(t *testing.T) {
type s struct {
Foo int
Bar struct{ Baz string }
}
var v s
u := bind.BindFields(
&v,
bind.ValueByPath([]string{"Foo"}, 42),
bind.ValueByPath([]string{"Bar", "Baz"}, "qux"),
)
if len(u) != 0 || v.Foo != 42 || v.Bar.Baz != "qux" {
t.Fatal()
}
})
t.Run("cannot set field", func(t *testing.T) {
type s struct{ Foo int }
var v s
u := bind.BindFields(v, bind.NamedValue("foo", 42))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("struct with anonymous field", func(t *testing.T) {
type (
s0 struct{ Foo int }
s1 struct{ s0 }
)
var v s1
u := bind.BindFields(&v, bind.NamedValue("foo", 42))
if len(u) != 0 || v.Foo != 42 {
t.Fatal()
}
})
t.Run("receiver cannot be set", func(t *testing.T) {
type s struct{ Foo *struct{ Bar int } }
var v s
u := bind.BindFields(v, bind.NamedValue("foo-bar", 42))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("receiver not supported", func(t *testing.T) {
v := make(chan int)
u := bind.BindFields(&v, bind.NamedValue("foo-bar", 42))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("empty receiver", func(t *testing.T) {
var v any
u := bind.BindFields(&v, bind.NamedValue("foo", 42))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("nil value", func(t *testing.T) {
type s struct{ Foo any }
v := s{42}
u := bind.BindFields(&v, bind.NamedValue("foo", nil))
if len(u) != 0 || v.Foo != nil {
t.Fatal()
}
})
t.Run("nil receiver", func(t *testing.T) {
u := bind.BindFields(nil, bind.NamedValue("foo", nil))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("ambigously named fields", func(t *testing.T) {
type s struct {
FooBar int
Foo struct{ Bar int }
}
var v s
u := bind.BindFields(&v, bind.NamedValue("foo-bar", 42))
if len(u) != 0 || v.FooBar != 42 || v.Foo.Bar != 42 {
t.Fatal(notation.Sprint(u), notation.Sprint(v))
}
})
})
t.Run("bind fields create", func(t *testing.T) {
t.Run("circular type", func(t *testing.T) {
type s struct {
Foo int
Bar *s
}
_, u := bind.BindFieldsCreate[s](bind.NamedValue("foo", 42))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("type does not accept fields", func(t *testing.T) {
_, u := bind.BindFieldsCreate[[]int](bind.NamedValue("foo", 42))
if len(u) != 1 {
t.Fatal()
}
})
t.Run("zero value when no fields were bound", func(t *testing.T) {
type s struct{ Foo int }
v, u := bind.BindFieldsCreate[*s](bind.NamedValue("bar", 42))
if len(u) != 1 || v != nil {
t.Fatal()
}
})
t.Run("create receiver", func(t *testing.T) {
type s struct{ Foo int }
v, u := bind.BindFieldsCreate[*s](bind.NamedValue("foo", 42))
if len(u) != 0 || v == nil || v.Foo != 42 {
t.Fatal()
}
})
t.Run("any type", func(t *testing.T) {
_, u := bind.BindFieldsCreate[any](bind.NamedValue("foo", 42))
if len(u) != 1 {
t.Fatal()
}
})
})
}