field matching

g-counter
Jordan Orelli 4 years ago
parent e1fc2529e9
commit 814dbea1bb

@ -49,6 +49,11 @@ func (e *env) load(dest Test) error {
destV := reflect.ValueOf(dest).Elem() destV := reflect.ValueOf(dest).Elem()
destT := destV.Type() destT := destV.Type()
e = e.match(dest)
if e == nil {
return fmt.Errorf("failed to find a matching environment")
}
for i := 0; i < destT.NumField(); i++ { for i := 0; i < destT.NumField(); i++ {
f := destT.Field(i) f := destT.Field(i)
if !isLoadField(f) { if !isLoadField(f) {
@ -77,3 +82,70 @@ func (e *env) load(dest Test) error {
} }
return nil return nil
} }
func (e *env) match(dest Test) *env {
destV := reflect.ValueOf(dest).Elem()
destT := destV.Type()
required := getMatchFields(destT)
if len(required) == 0 {
return e
}
var last *env
var leaf *env
for e := e; e != nil; e = e.parent {
present := make([]reflect.StructField, 0, len(required))
for _, f := range required {
ev, ok := e.data[f.Name]
if !ok {
break
}
if reflect.TypeOf(ev).AssignableTo(f.Type) {
present = append(present, f)
}
}
// all required fields are present in this layer
if len(present) == len(required) {
// check that the values in the env match the values that were
// asked for.
matched := make(map[string]interface{})
for _, f := range required {
fv := destV.FieldByName(f.Name)
if fv.Interface() == e.data[f.Name] {
matched[f.Name] = e.data[f.Name]
}
}
// all required match conditions are met
if len(matched) == len(required) {
if leaf == nil {
// if this is the first matched layer, it is the leaf of the
// resultant env.
leaf = e
last = leaf
} else {
// otherwise we keep this layer, since it matched our match
// requirements. Another layer already did, but there may
// be other things in the layer we want to keep.
last.parent = e
last = e
}
}
} else {
// the required fields do not exist in the layer, so this layer
// does not conflict with the match requirement.
if leaf != nil {
// since we have a leaf node, we have found a matching layer,
// and since this layer does not conflict, we keep it.
last.parent = e
last = e
}
}
}
return leaf
}

@ -5,18 +5,6 @@ import (
) )
func TestSave(t *testing.T) { func TestSave(t *testing.T) {
type saveFoo struct {
empty
Foo int `tea:"save"`
Bar string
}
type loadFoo struct {
empty
Foo int `tea:"load"`
Bar string
}
t.Run("empty begets nil", func(t *testing.T) { t.Run("empty begets nil", func(t *testing.T) {
e := mkenv(new(empty)) e := mkenv(new(empty))
if e != nil { if e != nil {
@ -35,8 +23,38 @@ func TestSave(t *testing.T) {
} }
}) })
t.Run("save an int", func(t *testing.T) { t.Run("create an env from a test", func(t *testing.T) {
e := mkenv(&saveFoo{Foo: 5}) test := struct {
empty
Foo int `tea:"save"`
}{
Foo: 5,
}
e := mkenv(&test)
if e == nil {
t.Fatalf("saw nil env when expecting a valid env")
}
foo, ok := e.data["Foo"]
if !ok {
t.Errorf("expected field Foo to be saved but was not saved")
}
if foo != 5 {
t.Errorf("expected value %v but saw %v instead", 5, foo)
}
})
t.Run("update an existing env", func(t *testing.T) {
test := struct {
empty
Foo int `tea:"save"`
}{
Foo: 5,
}
e := mkenv(&test)
if e == nil { if e == nil {
t.Fatalf("saw nil env when expecting a valid env") t.Fatalf("saw nil env when expecting a valid env")
} }
@ -50,22 +68,147 @@ func TestSave(t *testing.T) {
t.Errorf("expected value %v but saw %v instead", 5, foo) t.Errorf("expected value %v but saw %v instead", 5, foo)
} }
}) })
}
func TestLoad(t *testing.T) {
t.Run("load an int", func(t *testing.T) { t.Run("load an int", func(t *testing.T) {
e := mkenv(&saveFoo{Foo: 5}) e := &env{
test := new(loadFoo) data: map[string]interface{}{"Foo": 5},
}
var test struct {
empty
Foo int `tea:"load"`
}
e.load(test) e.load(&test)
if test.Foo != 5 { if test.Foo != 5 {
t.Errorf("expected value %v but saw %v instead", 5, test.Foo) t.Errorf("expected value %v but saw %v instead", 5, test.Foo)
} }
}) })
t.Run("loads can fail", func(t *testing.T) { t.Run("loads can fail", func(t *testing.T) {
e := mkenv(new(empty)) e := &env{
test := new(loadFoo) data: map[string]interface{}{"NotFoo": 5},
if err := e.load(test); err == nil { }
var test struct {
empty
Foo int `tea:"load"`
}
if err := e.load(&test); err == nil {
t.Errorf("expected a load error but did not see one")
}
})
}
func TestMatch(t *testing.T) {
t.Run("required match field not present", func(t *testing.T) {
e := &env{
data: map[string]interface{}{"Foo": 5},
}
var test struct {
empty
Name string `tea:"match"`
Foo int `tea:"load"`
}
if err := e.load(&test); err == nil {
t.Errorf("expected a load error but did not see one")
}
})
t.Run("required match field has wrong value", func(t *testing.T) {
e := &env{
data: map[string]interface{}{
"Foo": 5,
"Name": "alice",
},
}
var test struct {
empty
Name string `tea:"match"`
Foo int `tea:"load"`
}
test.Name = "bob"
if err := e.load(&test); err == nil {
t.Errorf("expected a load error but did not see one") t.Errorf("expected a load error but did not see one")
} }
}) })
t.Run("simple match", func(t *testing.T) {
e := &env{
data: map[string]interface{}{
"Foo": 5,
"Name": "alice",
},
}
var test struct {
empty
Name string `tea:"match"`
Foo int `tea:"load"`
}
test.Name = "alice"
if err := e.load(&test); err != nil {
t.Errorf("unexpected load error: %v", err)
}
if test.Foo != 5 {
t.Errorf("expected Foo to load 5 but is %d instead", test.Foo)
}
})
t.Run("ancestor match", func(t *testing.T) {
e := &env{
data: map[string]interface{}{
"Foo": 3,
"Name": "bob",
},
parent: &env{
data: map[string]interface{}{
"Foo": 5,
"Name": "alice",
},
},
}
var test struct {
empty
Name string `tea:"match"`
Foo int `tea:"load"`
}
test.Name = "alice"
if err := e.load(&test); err != nil {
t.Errorf("unexpected load error: %v", err)
}
if test.Foo != 5 {
t.Errorf("expected Foo to load 5 but is %d instead", test.Foo)
}
})
} }
// A.Optional(B).Child(C)
//
// A A
// /| / \
// / | / \
// B | ----> B C'
// \ | |
// \| |
// C C
// what to call this thing?
//
// A A
// / \ / \
// / \ / \
// B C ----> B C
// \ / | |
// \ / | |
// D D D'

@ -130,3 +130,28 @@ func isLoadField(f reflect.StructField) bool {
} }
return false return false
} }
func isMatchField(f reflect.StructField) bool {
// PkgPath is empty string when the identifier is unexported.
if f.PkgPath != "" {
return false
}
parts := strings.Split(f.Tag.Get("tea"), ",")
for _, part := range parts {
if part == "match" {
return true
}
}
return false
}
func getMatchFields(t reflect.Type) []reflect.StructField {
var fields []reflect.StructField
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
if isMatchField(f) {
fields = append(fields, f)
}
}
return fields
}

Loading…
Cancel
Save