diff --git a/structure.go b/structure.go index 9eadd17..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{} { @@ -34,18 +41,18 @@ 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()) } else { - 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 + finalVal = val.Interface() } out[name] = finalVal @@ -61,15 +68,26 @@ 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{} { 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()) { @@ -84,6 +102,44 @@ 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:"-"` +// +// 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 { + 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: @@ -91,6 +147,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 IsZero(s interface{}) bool { @@ -99,7 +162,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 @@ -129,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 { @@ -136,7 +208,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 @@ -159,34 +234,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_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 diff --git a/structure_test.go b/structure_test.go index 3707216..f3d1e51 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 @@ -246,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 @@ -340,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 @@ -440,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 @@ -539,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 new file mode 100644 index 0000000..c370af0 --- /dev/null +++ b/tags.go @@ -0,0 +1,40 @@ +package structure + +import "strings" + +// tagOptions contains a slice of tag options +type tagOptions []string + +// 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 { + return true + } + } + + return false +} + +// parseTag splits a struct field's tag into its name and a list of options +// 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, ",") + + // 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) + } + } +}