| // Copyright 2016 Google LLC |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package fields |
| |
| import ( |
| "encoding/json" |
| "errors" |
| "fmt" |
| "reflect" |
| "testing" |
| "time" |
| |
| "cloud.google.com/go/internal/testutil" |
| "github.com/google/go-cmp/cmp" |
| ) |
| |
| type embed1 struct { |
| Em1 int |
| Dup int // annihilates with embed2.Dup |
| Shadow int |
| embed3 |
| } |
| |
| type embed2 struct { |
| Dup int |
| embed3 |
| embed4 |
| } |
| |
| type embed3 struct { |
| Em3 int // annihilated because embed3 is in both embed1 and embed2 |
| embed5 |
| } |
| |
| type embed4 struct { |
| Em4 int |
| Dup int // annihilation of Dup in embed1, embed2 hides this Dup |
| *embed1 // ignored because it occurs at a higher level |
| } |
| |
| type embed5 struct { |
| x int |
| } |
| |
| type Anonymous int |
| |
| type S1 struct { |
| Exported int |
| unexported int |
| Shadow int // shadows S1.Shadow |
| embed1 |
| *embed2 |
| Anonymous |
| } |
| |
| type Time struct { |
| time.Time |
| } |
| |
| var intType = reflect.TypeOf(int(0)) |
| |
| func field(name string, tval interface{}, index ...int) *Field { |
| return &Field{ |
| Name: name, |
| Type: reflect.TypeOf(tval), |
| Index: index, |
| ParsedTag: []string(nil), |
| } |
| } |
| |
| func tfield(name string, tval interface{}, index ...int) *Field { |
| return &Field{ |
| Name: name, |
| Type: reflect.TypeOf(tval), |
| Index: index, |
| NameFromTag: true, |
| ParsedTag: []string(nil), |
| } |
| } |
| |
| func TestFieldsNoTags(t *testing.T) { |
| c := NewCache(nil, nil, nil) |
| got, err := c.Fields(reflect.TypeOf(S1{})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| want := []*Field{ |
| field("Exported", int(0), 0), |
| field("Shadow", int(0), 2), |
| field("Em1", int(0), 3, 0), |
| field("Em4", int(0), 4, 2, 0), |
| field("Anonymous", Anonymous(0), 5), |
| } |
| for _, f := range want { |
| f.ParsedTag = nil |
| } |
| if msg, ok := compareFields(got, want); !ok { |
| t.Error(msg) |
| } |
| } |
| |
| func TestAgainstJSONEncodingNoTags(t *testing.T) { |
| // Demonstrates that this package produces the same set of fields as encoding/json. |
| s1 := S1{ |
| Exported: 1, |
| unexported: 2, |
| Shadow: 3, |
| embed1: embed1{ |
| Em1: 4, |
| Dup: 5, |
| Shadow: 6, |
| embed3: embed3{ |
| Em3: 7, |
| embed5: embed5{x: 8}, |
| }, |
| }, |
| embed2: &embed2{ |
| Dup: 9, |
| embed3: embed3{ |
| Em3: 10, |
| embed5: embed5{x: 11}, |
| }, |
| embed4: embed4{ |
| Em4: 12, |
| Dup: 13, |
| embed1: &embed1{Em1: 14}, |
| }, |
| }, |
| Anonymous: Anonymous(15), |
| } |
| var want S1 |
| want.embed2 = &embed2{} // need this because reflection won't create it |
| jsonRoundTrip(t, s1, &want) |
| var got S1 |
| got.embed2 = &embed2{} |
| fields, err := NewCache(nil, nil, nil).Fields(reflect.TypeOf(got)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| setFields(fields, &got, s1) |
| if !testutil.Equal(got, want, |
| cmp.AllowUnexported(S1{}, embed1{}, embed2{}, embed3{}, embed4{}, embed5{})) { |
| t.Errorf("got\n%+v\nwant\n%+v", got, want) |
| } |
| } |
| |
| // Tests use of LeafTypes parameter to NewCache |
| func TestAgainstJSONEncodingEmbeddedTime(t *testing.T) { |
| timeLeafFn := func(t reflect.Type) bool { |
| return t == reflect.TypeOf(time.Time{}) |
| } |
| // Demonstrates that this package can produce the same set of |
| // fields as encoding/json for a struct with an embedded time.Time. |
| now := time.Now().UTC() |
| myt := Time{ |
| now, |
| } |
| var want Time |
| jsonRoundTrip(t, myt, &want) |
| var got Time |
| fields, err := NewCache(nil, nil, timeLeafFn).Fields(reflect.TypeOf(got)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| setFields(fields, &got, myt) |
| if !testutil.Equal(got, want) { |
| t.Errorf("got\n%+v\nwant\n%+v", got, want) |
| } |
| } |
| |
| type S2 struct { |
| NoTag int |
| XXX int `json:"tag"` // tag name takes precedence |
| Anonymous `json:"anon"` // anonymous non-structs also get their name from the tag |
| Embed `json:"em"` // embedded structs with tags become fields |
| Tag int |
| YYY int `json:"Tag"` // tag takes precedence over untagged field of the same name |
| Empty int `json:""` // empty tag is noop |
| tEmbed1 |
| tEmbed2 |
| } |
| |
| type Embed struct { |
| Em int |
| } |
| |
| type tEmbed1 struct { |
| Dup int |
| X int `json:"Dup2"` |
| } |
| |
| type tEmbed2 struct { |
| Y int `json:"Dup"` // takes precedence over tEmbed1.Dup because it is tagged |
| Z int `json:"Dup2"` // same name as tEmbed1.X and both tagged, so ignored |
| } |
| |
| func jsonTagParser(t reflect.StructTag) (name string, keep bool, other interface{}, err error) { |
| return ParseStandardTag("json", t) |
| } |
| |
| func validateFunc(t reflect.Type) (err error) { |
| if t.Kind() != reflect.Struct { |
| return errors.New("non-struct type used") |
| } |
| |
| for i := 0; i < t.NumField(); i++ { |
| if t.Field(i).Type.Kind() == reflect.Slice { |
| return fmt.Errorf("slice field found at field %s on struct %s", t.Field(i).Name, t.Name()) |
| } |
| } |
| |
| return nil |
| } |
| |
| func TestFieldsWithTags(t *testing.T) { |
| got, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S2{})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| want := []*Field{ |
| field("NoTag", int(0), 0), |
| tfield("tag", int(0), 1), |
| tfield("anon", Anonymous(0), 2), |
| tfield("em", Embed{}, 4), |
| tfield("Tag", int(0), 6), |
| field("Empty", int(0), 7), |
| tfield("Dup", int(0), 8, 0), |
| } |
| if msg, ok := compareFields(got, want); !ok { |
| t.Error(msg) |
| } |
| } |
| |
| func TestAgainstJSONEncodingWithTags(t *testing.T) { |
| // Demonstrates that this package produces the same set of fields as encoding/json. |
| s2 := S2{ |
| NoTag: 1, |
| XXX: 2, |
| Anonymous: 3, |
| Embed: Embed{ |
| Em: 4, |
| }, |
| tEmbed1: tEmbed1{ |
| Dup: 5, |
| X: 6, |
| }, |
| tEmbed2: tEmbed2{ |
| Y: 7, |
| Z: 8, |
| }, |
| } |
| var want S2 |
| jsonRoundTrip(t, s2, &want) |
| var got S2 |
| fields, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(got)) |
| if err != nil { |
| t.Fatal(err) |
| } |
| setFields(fields, &got, s2) |
| if !testutil.Equal(got, want, cmp.AllowUnexported(S2{})) { |
| t.Errorf("got\n%+v\nwant\n%+v", got, want) |
| } |
| } |
| |
| func TestUnexportedAnonymousNonStruct(t *testing.T) { |
| // An unexported anonymous non-struct field should not be recorded. |
| // This is currently a bug in encoding/json. |
| // https://github.com/golang/go/issues/18009 |
| type S struct{} |
| |
| got, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S{})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(got) != 0 { |
| t.Errorf("got %d fields, want 0", len(got)) |
| } |
| } |
| |
| func TestUnexportedAnonymousStruct(t *testing.T) { |
| // An unexported anonymous struct with a tag is ignored. |
| // This is currently a bug in encoding/json. |
| // https://github.com/golang/go/issues/18009 |
| type ( |
| s1 struct{ X int } |
| S2 struct { |
| s1 `json:"Y"` |
| } |
| ) |
| got, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S2{})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(got) != 0 { |
| t.Errorf("got %d fields, want 0", len(got)) |
| } |
| } |
| |
| func TestDominantField(t *testing.T) { |
| // With fields sorted by index length and then by tag presence, |
| // the dominant field is always the first. Make sure all error |
| // cases are caught. |
| for _, test := range []struct { |
| fields []Field |
| wantOK bool |
| }{ |
| // A single field is OK. |
| {[]Field{{Index: []int{0}}}, true}, |
| {[]Field{{Index: []int{0}, NameFromTag: true}}, true}, |
| // A single field at top level is OK. |
| {[]Field{{Index: []int{0}}, {Index: []int{1, 0}}}, true}, |
| {[]Field{{Index: []int{0}}, {Index: []int{1, 0}, NameFromTag: true}}, true}, |
| {[]Field{{Index: []int{0}, NameFromTag: true}, {Index: []int{1, 0}, NameFromTag: true}}, true}, |
| // A single tagged field is OK. |
| {[]Field{{Index: []int{0}, NameFromTag: true}, {Index: []int{1}}}, true}, |
| // Two untagged fields at the same level is an error. |
| {[]Field{{Index: []int{0}}, {Index: []int{1}}}, false}, |
| // Two tagged fields at the same level is an error. |
| {[]Field{{Index: []int{0}, NameFromTag: true}, {Index: []int{1}, NameFromTag: true}}, false}, |
| } { |
| _, gotOK := dominantField(test.fields) |
| if gotOK != test.wantOK { |
| t.Errorf("%v: got %t, want %t", test.fields, gotOK, test.wantOK) |
| } |
| } |
| } |
| |
| func TestIgnore(t *testing.T) { |
| type S struct { |
| X int `json:"-"` |
| } |
| got, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S{})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if len(got) != 0 { |
| t.Errorf("got %d fields, want 0", len(got)) |
| } |
| } |
| |
| func TestParsedTag(t *testing.T) { |
| type S struct { |
| X int `json:"name,omitempty"` |
| } |
| got, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S{})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| want := []*Field{ |
| {Name: "name", NameFromTag: true, Type: intType, |
| Index: []int{0}, ParsedTag: []string{"omitempty"}}, |
| } |
| if msg, ok := compareFields(got, want); !ok { |
| t.Error(msg) |
| } |
| } |
| |
| func TestValidateFunc(t *testing.T) { |
| type MyInvalidStruct struct { |
| A string |
| B []int |
| } |
| |
| _, err := NewCache(nil, validateFunc, nil).Fields(reflect.TypeOf(MyInvalidStruct{})) |
| if err == nil { |
| t.Fatal("expected error, got nil") |
| } |
| |
| type MyValidStruct struct { |
| A string |
| B int |
| } |
| _, err = NewCache(nil, validateFunc, nil).Fields(reflect.TypeOf(MyValidStruct{})) |
| if err != nil { |
| t.Fatalf("expected nil, got error: %s\n", err) |
| } |
| } |
| |
| func compareFields(got []Field, want []*Field) (msg string, ok bool) { |
| if len(got) != len(want) { |
| return fmt.Sprintf("got %d fields, want %d", len(got), len(want)), false |
| } |
| for i, g := range got { |
| w := *want[i] |
| if !fieldsEqual(&g, &w) { |
| return fmt.Sprintf("got\n%+v\nwant\n%+v", g, w), false |
| } |
| } |
| return "", true |
| } |
| |
| // Need this because Field contains a function, which cannot be compared even |
| // by testutil.Equal. |
| func fieldsEqual(f1, f2 *Field) bool { |
| if f1 == nil || f2 == nil { |
| return f1 == f2 |
| } |
| return f1.Name == f2.Name && |
| f1.NameFromTag == f2.NameFromTag && |
| f1.Type == f2.Type && |
| testutil.Equal(f1.ParsedTag, f2.ParsedTag) |
| } |
| |
| // Set the fields of dst from those of src. |
| // dst must be a pointer to a struct value. |
| // src must be a struct value. |
| func setFields(fields []Field, dst, src interface{}) { |
| vsrc := reflect.ValueOf(src) |
| vdst := reflect.ValueOf(dst).Elem() |
| for _, f := range fields { |
| fdst := vdst.FieldByIndex(f.Index) |
| fsrc := vsrc.FieldByIndex(f.Index) |
| fdst.Set(fsrc) |
| } |
| } |
| |
| func jsonRoundTrip(t *testing.T, in, out interface{}) { |
| bytes, err := json.Marshal(in) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := json.Unmarshal(bytes, out); err != nil { |
| t.Fatal(err) |
| } |
| } |
| |
| type S3 struct { |
| S4 |
| Abc int |
| AbC int |
| Tag int |
| X int `json:"Tag"` |
| unexported int |
| } |
| |
| type S4 struct { |
| ABc int |
| Y int `json:"Abc"` // ignored because of top-level Abc |
| } |
| |
| func TestMatchingField(t *testing.T) { |
| fields, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S3{})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| for _, test := range []struct { |
| name string |
| want *Field |
| }{ |
| // Exact match wins. |
| {"Abc", field("Abc", int(0), 1)}, |
| {"AbC", field("AbC", int(0), 2)}, |
| {"ABc", field("ABc", int(0), 0, 0)}, |
| // If there are multiple matches but no exact match or tag, |
| // the first field wins, lexicographically by index. |
| // Here, "ABc" is at a deeper embedding level, but since S4 appears |
| // first in S3, its index precedes the other fields of S3. |
| {"abc", field("ABc", int(0), 0, 0)}, |
| // Tag name takes precedence over untagged field of the same name. |
| {"Tag", tfield("Tag", int(0), 4)}, |
| // Unexported fields disappear. |
| {"unexported", nil}, |
| // Untagged embedded structs disappear. |
| {"S4", nil}, |
| } { |
| if got := fields.Match(test.name); !fieldsEqual(got, test.want) { |
| t.Errorf("match %q:\ngot %+v\nwant %+v", test.name, got, test.want) |
| } |
| } |
| } |
| |
| func TestAgainstJSONMatchingField(t *testing.T) { |
| s3 := S3{ |
| S4: S4{ABc: 1, Y: 2}, |
| Abc: 3, |
| AbC: 4, |
| Tag: 5, |
| X: 6, |
| unexported: 7, |
| } |
| var want S3 |
| jsonRoundTrip(t, s3, &want) |
| v := reflect.ValueOf(want) |
| fields, err := NewCache(jsonTagParser, nil, nil).Fields(reflect.TypeOf(S3{})) |
| if err != nil { |
| t.Fatal(err) |
| } |
| for _, test := range []struct { |
| name string |
| got int |
| }{ |
| {"Abc", 3}, |
| {"AbC", 4}, |
| {"ABc", 1}, |
| {"abc", 1}, |
| {"Tag", 6}, |
| } { |
| f := fields.Match(test.name) |
| if f == nil { |
| t.Fatalf("%s: no match", test.name) |
| } |
| w := v.FieldByIndex(f.Index).Interface() |
| if test.got != w { |
| t.Errorf("%s: got %d, want %d", test.name, test.got, w) |
| } |
| } |
| } |
| |
| func TestTagErrors(t *testing.T) { |
| called := false |
| c := NewCache(func(t reflect.StructTag) (string, bool, interface{}, error) { |
| called = true |
| s := t.Get("f") |
| if s == "bad" { |
| return "", false, nil, errors.New("error") |
| } |
| return s, true, nil, nil |
| }, nil, nil) |
| |
| type T struct { |
| X int `f:"ok"` |
| Y int `f:"bad"` |
| } |
| |
| _, err := c.Fields(reflect.TypeOf(T{})) |
| if !called { |
| t.Fatal("tag parser not called") |
| } |
| if err == nil { |
| t.Error("want error, got nil") |
| } |
| // Second time, we should cache the error. |
| called = false |
| _, err = c.Fields(reflect.TypeOf(T{})) |
| if called { |
| t.Fatal("tag parser called on second time") |
| } |
| if err == nil { |
| t.Error("want error, got nil") |
| } |
| } |