From fc5199c8cc0393a6c88c020e475381b7bfb67220 Mon Sep 17 00:00:00 2001 From: Fatih Arslan Date: Thu, 7 Aug 2014 18:17:41 +0300 Subject: [PATCH 1/6] tags: parse tag names and options --- tags.go | 39 +++++++++++++++++++++++++++++++++++++++ tags_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 tags.go create mode 100644 tags_test.go diff --git a/tags.go b/tags.go new file mode 100644 index 0000000..c096728 --- /dev/null +++ b/tags.go @@ -0,0 +1,39 @@ +package structure + +import "strings" + +// tagOptions contains a slice of tag options +type tagOptions []string + +// Has returns true if the given opt is available inside the slice. +func (t tagOptions) Has(opt string) bool { + for _, tagOpt := range t { + if tagOpt == opt { + return true + } + } + + return false +} + +// parseTag splits a struct field's tag into its name and a list of options +// which comes after a name +func parseTag(tag string) (string, tagOptions) { + res := strings.Split(tag, ",") + + // tag = "" + if len(res) == 0 { + return tag, res + } + + // tag = "name" + if len(res) == 1 { + return tag, res[1:] + } + + // tag is one of followings: + // "name,opt" + // "name,opt,opt2" + // ",opt" + return res[0], res[1:] +} diff --git a/tags_test.go b/tags_test.go new file mode 100644 index 0000000..d914241 --- /dev/null +++ b/tags_test.go @@ -0,0 +1,45 @@ +package structure + +import "testing" + +func TestParseTag_Name(t *testing.T) { + tags := []struct { + tag string + has bool + }{ + {"name", true}, + {"name,opt", true}, + {"name , opt, opt2", false}, // has a single whitespace + {", opt, opt2", false}, + } + + for _, tag := range tags { + name, _ := parseTag(tag.tag) + + if (name != "name") && tag.has { + t.Errorf("Parse tag should return name: %#v", tag) + } + } +} + +func TestParseTag_Opts(t *testing.T) { + tags := []struct { + opts string + has bool + }{ + {"name", false}, + {"name,opt", true}, + {"name , opt, opt2", false}, // has a single whitespace + {",opt, opt2", true}, + {", opt3, opt4", false}, + } + + // search for "opt" + for _, tag := range tags { + _, opts := parseTag(tag.opts) + + if opts.Has("opt") != tag.has { + t.Errorf("Tag opts should have opt: %#v", tag) + } + } +} From 137635f5eb48dfcf6df35f393a48e0e850059cdc Mon Sep 17 00:00:00 2001 From: Fatih Arslan Date: Thu, 7 Aug 2014 21:23:01 +0300 Subject: [PATCH 2/6] structure: add omitnested feature to Map --- structure.go | 13 ++++++------- structure_test.go | 32 ++++++++++++++++++++++++++++++++ tags.go | 3 ++- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/structure.go b/structure.go index 9eadd17..5200e23 100644 --- a/structure.go +++ b/structure.go @@ -34,7 +34,12 @@ func Map(s interface{}) map[string]interface{} { var finalVal interface{} - if IsStruct(val.Interface()) { + tagName, tagOpts := parseTag(field.Tag.Get(DefaultTagName)) + if tagName != "" { + name = tagName + } + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { // look out for embedded structs, and convert them to a // map[string]interface{} too finalVal = Map(val.Interface()) @@ -42,12 +47,6 @@ func Map(s interface{}) map[string]interface{} { finalVal = val.Interface() } - // override if the user passed a structure tag value - // ignore if the user passed the "-" value - if tag := field.Tag.Get(DefaultTagName); tag != "" { - name = tag - } - out[name] = finalVal } diff --git a/structure_test.go b/structure_test.go index 3707216..0ea3946 100644 --- a/structure_test.go +++ b/structure_test.go @@ -3,6 +3,7 @@ package structure import ( "reflect" "testing" + "time" ) func TestMapNonStruct(t *testing.T) { @@ -145,6 +146,37 @@ func TestMap_CustomTag(t *testing.T) { } +func TestMap_OmitNested(t *testing.T) { + type A struct { + Name string + Value string + Time time.Time `structure:",omitnested"` + } + a := A{Time: time.Now()} + + type B struct { + Desc string + A A + } + b := &B{A: a} + + m := Map(b) + + in, ok := m["A"].(map[string]interface{}) + if !ok { + t.Error("Map nested structs is not available in the map") + } + + // should not happen + if _, ok := in["Time"].(map[string]interface{}); ok { + t.Error("Map nested struct should omit recursiving parsing of Time") + } + + if _, ok := in["Time"].(time.Time); !ok { + t.Error("Map nested struct should stop parsing of Time at is current value") + } +} + func TestMap_Nested(t *testing.T) { type A struct { Name string diff --git a/tags.go b/tags.go index c096728..5a3a8b0 100644 --- a/tags.go +++ b/tags.go @@ -17,7 +17,8 @@ func (t tagOptions) Has(opt string) bool { } // parseTag splits a struct field's tag into its name and a list of options -// which comes after a name +// which comes after a name. A tag is in the form of: "name,option1,option2". +// The name can be neglectected. func parseTag(tag string) (string, tagOptions) { res := strings.Split(tag, ",") From eacf1a759041640bdcca8670cd066684a0335698 Mon Sep 17 00:00:00 2001 From: Fatih Arslan Date: Thu, 7 Aug 2014 21:57:51 +0300 Subject: [PATCH 3/6] structure: add omitnested support to values --- structure.go | 7 ++++++- structure_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/structure.go b/structure.go index 5200e23..c4174fc 100644 --- a/structure.go +++ b/structure.go @@ -44,6 +44,7 @@ func Map(s interface{}) map[string]interface{} { // map[string]interface{} too finalVal = Map(val.Interface()) } else { + finalVal = val.Interface() } @@ -66,9 +67,13 @@ func Values(s interface{}) []interface{} { v, fields := strctInfo(s) t := make([]interface{}, 0) + for _, field := range fields { val := v.FieldByName(field.Name) - if IsStruct(val.Interface()) { + + _, tagOpts := parseTag(field.Tag.Get(DefaultTagName)) + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { // look out for embedded structs, and convert them to a // []interface{} to be added to the final values slice for _, embeddedVal := range Values(val.Interface()) { diff --git a/structure_test.go b/structure_test.go index 0ea3946..1ab8e7e 100644 --- a/structure_test.go +++ b/structure_test.go @@ -278,6 +278,45 @@ func TestValues(t *testing.T) { } } +func TestValues_OmitNested(t *testing.T) { + type A struct { + Name string + Value int + } + + a := A{ + Name: "example", + Value: 123, + } + + type B struct { + A A `structure:",omitnested"` + C int + } + b := &B{A: a, C: 123} + + s := Values(b) + + if len(s) != 2 { + t.Errorf("Values of omitted nested struct should be not counted") + } + + inSlice := func(val interface{}) bool { + for _, v := range s { + if reflect.DeepEqual(v, val) { + return true + } + } + return false + } + + for _, val := range []interface{}{123, a} { + if !inSlice(val) { + t.Errorf("Values should have the value %v", val) + } + } +} + func TestValues_Nested(t *testing.T) { type A struct { Name string From 72596462dda28831853450d90a5cd7bc02df7d26 Mon Sep 17 00:00:00 2001 From: Fatih Arslan Date: Thu, 7 Aug 2014 22:18:04 +0300 Subject: [PATCH 4/6] structure: add omitnested support to Fields --- structure.go | 59 +++++++++++++++++++++++++---------------------- structure_test.go | 37 +++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 28 deletions(-) diff --git a/structure.go b/structure.go index c4174fc..589f7e2 100644 --- a/structure.go +++ b/structure.go @@ -88,6 +88,37 @@ func Values(s interface{}) []interface{} { } +// Fields returns a slice of field names. A struct tag with the content of "-" +// ignores the checking of that particular field. Example: +// +// // Field is ignored by this package. +// Field bool `structure:"-"` +// +// Note that only exported fields of a struct can be accessed, non exported +// fields will be neglected. It panics if s's kind is not struct. +func Fields(s interface{}) []string { + v, fields := strctInfo(s) + + keys := make([]string, 0) + for _, field := range fields { + val := v.FieldByName(field.Name) + + _, tagOpts := parseTag(field.Tag.Get(DefaultTagName)) + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { + // look out for embedded structs, and convert them to a + // []string to be added to the final values slice + for _, embeddedVal := range Fields(val.Interface()) { + keys = append(keys, embeddedVal) + } + } + + keys = append(keys, field.Name) + } + + return keys +} + // IsZero returns true if all fields in a struct is a zero value (not // initialized) A struct tag with the content of "-" ignores the checking of // that particular field. Example: @@ -163,34 +194,6 @@ func HasZero(s interface{}) bool { return false } -// Fields returns a slice of field names. A struct tag with the content of "-" -// ignores the checking of that particular field. Example: -// -// // Field is ignored by this package. -// Field bool `structure:"-"` -// -// Note that only exported fields of a struct can be accessed, non exported -// fields will be neglected. It panics if s's kind is not struct. -func Fields(s interface{}) []string { - v, fields := strctInfo(s) - - keys := make([]string, 0) - for _, field := range fields { - val := v.FieldByName(field.Name) - if IsStruct(val.Interface()) { - // look out for embedded structs, and convert them to a - // []string to be added to the final values slice - for _, embeddedVal := range Fields(val.Interface()) { - keys = append(keys, embeddedVal) - } - } - - keys = append(keys, field.Name) - } - - return keys -} - // IsStruct returns true if the given variable is a struct or a pointer to // struct. func IsStruct(s interface{}) bool { diff --git a/structure_test.go b/structure_test.go index 1ab8e7e..5d14a3a 100644 --- a/structure_test.go +++ b/structure_test.go @@ -411,6 +411,43 @@ func TestFields(t *testing.T) { } } +func TestFields_OmitNested(t *testing.T) { + type A struct { + Name string + Value string + Number int + Enabled bool + } + a := A{Name: "example"} + + type B struct { + A A `structure:",omitnested"` + C int + } + b := &B{A: a, C: 123} + + s := Fields(b) + + if len(s) != 2 { + t.Errorf("Fields should omit nested struct. Expecting 2 got: %d", len(s)) + } + + inSlice := func(val interface{}) bool { + for _, v := range s { + if reflect.DeepEqual(v, val) { + return true + } + } + return false + } + + for _, val := range []interface{}{"A", "C"} { + if !inSlice(val) { + t.Errorf("Fields should have the value %v", val) + } + } +} + func TestFields_Nested(t *testing.T) { type A struct { Name string From 1b32eb13165e1d48943fc3c46d09c4af795b8c57 Mon Sep 17 00:00:00 2001 From: Fatih Arslan Date: Fri, 8 Aug 2014 11:49:06 +0300 Subject: [PATCH 5/6] structure: add omitnested support to IsZero and HasZero --- structure.go | 9 +++++++-- structure_test.go | 49 +++++++++++++++++++++++++++++++++++++++++++++++ tags.go | 2 +- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/structure.go b/structure.go index 589f7e2..d755151 100644 --- a/structure.go +++ b/structure.go @@ -134,7 +134,9 @@ func IsZero(s interface{}) bool { for _, field := range fields { val := v.FieldByName(field.Name) - if IsStruct(val.Interface()) { + _, tagOpts := parseTag(field.Tag.Get(DefaultTagName)) + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { ok := IsZero(val.Interface()) if !ok { return false @@ -171,7 +173,10 @@ func HasZero(s interface{}) bool { for _, field := range fields { val := v.FieldByName(field.Name) - if IsStruct(val.Interface()) { + + _, tagOpts := parseTag(field.Tag.Get(DefaultTagName)) + + if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") { ok := HasZero(val.Interface()) if ok { return true diff --git a/structure_test.go b/structure_test.go index 5d14a3a..f3d1e51 100644 --- a/structure_test.go +++ b/structure_test.go @@ -548,6 +548,34 @@ func TestIsZero(t *testing.T) { } } +func TestIsZero_OmitNested(t *testing.T) { + type A struct { + Name string + D string + } + a := A{Name: "example"} + + type B struct { + A A `structure:",omitnested"` + C int + } + b := &B{A: a, C: 123} + + ok := IsZero(b) + if ok { + t.Error("IsZero should return false because A, B and C are initialized") + } + + aZero := A{} + bZero := &B{A: aZero} + + ok = IsZero(bZero) + if !ok { + t.Error("IsZero should return true because neither A nor B is initialized") + } + +} + func TestIsZero_Nested(t *testing.T) { type A struct { Name string @@ -647,6 +675,27 @@ func TestHasZero(t *testing.T) { } } +func TestHasZero_OmitNested(t *testing.T) { + type A struct { + Name string + D string + } + a := A{Name: "example"} + + type B struct { + A A `structure:",omitnested"` + C int + } + b := &B{A: a, C: 123} + + // Because the Field A inside B is omitted HasZero should return false + // because it will stop iterating deeper andnot going to lookup for D + ok := HasZero(b) + if ok { + t.Error("HasZero should return false because A and C are initialized") + } +} + func TestHasZero_Nested(t *testing.T) { type A struct { Name string diff --git a/tags.go b/tags.go index 5a3a8b0..c370af0 100644 --- a/tags.go +++ b/tags.go @@ -5,7 +5,7 @@ import "strings" // tagOptions contains a slice of tag options type tagOptions []string -// Has returns true if the given opt is available inside the slice. +// Has returns true if the given optiton is available in tagOptions func (t tagOptions) Has(opt string) bool { for _, tagOpt := range t { if tagOpt == opt { From b10a55eb0c673145611b8115b2077b143b17251c Mon Sep 17 00:00:00 2001 From: Fatih Arslan Date: Fri, 8 Aug 2014 12:39:25 +0300 Subject: [PATCH 6/6] structure: clarifiy docs and add examples for omitnested tag option --- structure.go | 35 ++++++++++++++++ structure_example_test.go | 86 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/structure.go b/structure.go index d755151..e905dcf 100644 --- a/structure.go +++ b/structure.go @@ -21,6 +21,13 @@ var ( // // Field is ignored by this package. // Field bool `structure:"-"` // +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structure:"myName,omitnested"` +// Field *http.Request `structure:",omitnested"` +// // Note that only exported fields of a struct can be accessed, non exported // fields will be neglected. It panics if s's kind is not struct. func Map(s interface{}) map[string]interface{} { @@ -61,6 +68,13 @@ func Map(s interface{}) map[string]interface{} { // // Field is ignored by this package. // Field int `structure:"-"` // +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structure:"myName,omitnested"` +// Field *http.Request `structure:",omitnested"` +// // Note that only exported fields of a struct can be accessed, non exported // fields will be neglected. It panics if s's kind is not struct. func Values(s interface{}) []interface{} { @@ -94,6 +108,13 @@ func Values(s interface{}) []interface{} { // // Field is ignored by this package. // Field bool `structure:"-"` // +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structure:"myName,omitnested"` +// Field *http.Request `structure:",omitnested"` +// // Note that only exported fields of a struct can be accessed, non exported // fields will be neglected. It panics if s's kind is not struct. func Fields(s interface{}) []string { @@ -126,6 +147,13 @@ func Fields(s interface{}) []string { // // Field is ignored by this package. // Field bool `structure:"-"` // +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structure:"myName,omitnested"` +// Field *http.Request `structure:",omitnested"` +// // Note that only exported fields of a struct can be accessed, non exported // fields will be neglected. It panics if s's kind is not struct. func IsZero(s interface{}) bool { @@ -166,6 +194,13 @@ func IsZero(s interface{}) bool { // // Field is ignored by this package. // Field bool `structure:"-"` // +// A value with the option of "omitnested" stops iterating further if the type +// is a struct. Example: +// +// // Field is not processed further by this package. +// Field time.Time `structure:"myName,omitnested"` +// Field *http.Request `structure:",omitnested"` +// // Note that only exported fields of a struct can be accessed, non exported // fields will be neglected. It panics if s's kind is not struct. func HasZero(s interface{}) bool { diff --git a/structure_example_test.go b/structure_example_test.go index 6db6360..91ca3da 100644 --- a/structure_example_test.go +++ b/structure_example_test.go @@ -56,6 +56,36 @@ func ExampleMap_tags() { } +func ExampleMap_nested() { + // By default field with struct types are processed too. We can stop + // processing them via "omitnested" tag option. + type Server struct { + Name string `structure:"server_name"` + ID int32 `structure:"server_id"` + Time time.Time `structure:"time,omitnested"` // do not convert to map[string]interface{} + } + + const shortForm = "2006-Jan-02" + t, _ := time.Parse("2006-Jan-02", "2013-Feb-03") + + s := &Server{ + Name: "Zeynep", + ID: 789012, + Time: t, + } + + m := Map(s) + + // access them by the custom tags defined above + fmt.Printf("%v\n", m["server_name"]) + fmt.Printf("%v\n", m["server_id"]) + fmt.Printf("%v\n", m["time"].(time.Time)) + // Output: + // Zeynep + // 789012 + // 2013-02-03 00:00:00 +0000 UTC +} + func ExampleValues() { type Server struct { Name string @@ -76,6 +106,35 @@ func ExampleValues() { // Values: [Fatih 135790 false] } +func ExampleValues_tags() { + type Location struct { + City string + Country string + } + + type Server struct { + Name string + ID int32 + Enabled bool + Location Location `structure:"-"` // values from location are not included anymore + } + + s := &Server{ + Name: "Fatih", + ID: 135790, + Enabled: false, + Location: Location{City: "Ankara", Country: "Turkey"}, + } + + // Let get all values from the struct s. Note that we don't include values + // from the Location field + m := Values(s) + + fmt.Printf("Values: %+v\n", m) + // Output: + // Values: [Fatih 135790 false] +} + func ExampleFields() { type Access struct { Name string @@ -96,6 +155,33 @@ func ExampleFields() { // Fields: [Name LastAccessed Number] } +func ExampleFields_nested() { + type Person struct { + Name string + Number int + } + + type Access struct { + Person Person `structure:",omitnested"` + HasPermission bool + LastAccessed time.Time + } + + s := &Access{ + Person: Person{Name: "fatih", Number: 1234567}, + LastAccessed: time.Now(), + HasPermission: true, + } + + // Let's get all fields from the struct s. Note that we don't include the + // fields from the Person field anymore due to "omitnested" tag option. + m := Fields(s) + + fmt.Printf("Fields: %+v\n", m) + // Output: + // Fields: [Person HasPermission LastAccessed] +} + func ExampleIsZero() { type Server struct { Name string