From 814dbea1bb59e707bfda0dd00965a92c8a940544 Mon Sep 17 00:00:00 2001 From: Jordan Orelli Date: Tue, 28 Jul 2020 00:08:24 +0000 Subject: [PATCH] field matching --- env.go | 72 +++++++++++++++++++++ env_test.go | 183 ++++++++++++++++++++++++++++++++++++++++++++++------ tree.go | 25 +++++++ 3 files changed, 260 insertions(+), 20 deletions(-) diff --git a/env.go b/env.go index 33cdf43..27921da 100644 --- a/env.go +++ b/env.go @@ -49,6 +49,11 @@ func (e *env) load(dest Test) error { destV := reflect.ValueOf(dest).Elem() 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++ { f := destT.Field(i) if !isLoadField(f) { @@ -77,3 +82,70 @@ func (e *env) load(dest Test) error { } 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 +} diff --git a/env_test.go b/env_test.go index 0f01526..8bf9b57 100644 --- a/env_test.go +++ b/env_test.go @@ -5,18 +5,6 @@ import ( ) 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) { e := mkenv(new(empty)) if e != nil { @@ -35,8 +23,15 @@ func TestSave(t *testing.T) { } }) - t.Run("save an int", func(t *testing.T) { - e := mkenv(&saveFoo{Foo: 5}) + t.Run("create an env from a test", func(t *testing.T) { + 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") } @@ -51,21 +46,169 @@ func TestSave(t *testing.T) { } }) + 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 { + 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) + } + }) +} + +func TestLoad(t *testing.T) { t.Run("load an int", func(t *testing.T) { - e := mkenv(&saveFoo{Foo: 5}) - test := new(loadFoo) + e := &env{ + data: map[string]interface{}{"Foo": 5}, + } - e.load(test) + var test struct { + empty + Foo int `tea:"load"` + } + + e.load(&test) if test.Foo != 5 { t.Errorf("expected value %v but saw %v instead", 5, test.Foo) } }) t.Run("loads can fail", func(t *testing.T) { - e := mkenv(new(empty)) - test := new(loadFoo) - if err := e.load(test); err == nil { + e := &env{ + data: map[string]interface{}{"NotFoo": 5}, + } + + 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.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' diff --git a/tree.go b/tree.go index 4e9faaf..2b5051a 100644 --- a/tree.go +++ b/tree.go @@ -130,3 +130,28 @@ func isLoadField(f reflect.StructField) bool { } 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 +}