Merge pull request #10 from fatih/tags

structure: stop parsing nested structures via field tag
This commit is contained in:
Fatih Arslan 2014-08-08 12:42:00 +03:00
commit 76f7c2b740
5 changed files with 413 additions and 38 deletions

View File

@ -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 {

View File

@ -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

View File

@ -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

40
tags.go Normal file
View File

@ -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:]
}

45
tags_test.go Normal file
View File

@ -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)
}
}
}