diff --git a/README.md b/README.md index 80d9658..b5ecbfa 100644 --- a/README.md +++ b/README.md @@ -18,54 +18,126 @@ Lets define and declare a struct ```go type Server struct { - Name string - ID int32 - Enabled bool + Name string `json:"name,omitempty"` + ID int + Enabled bool + users []string // not exported + http.Server // embedded } -s := &Server{ +server := &Server{ Name: "gopher", ID: 123456, Enabled: true, } ``` +### Struct methods + +Let's create a new `Struct` type. + ```go +// Create a new struct type: +s := structure.New(server) + // Convert a struct to a map[string]interface{} // => {"Name":"gopher", "ID":123456, "Enabled":true} -m := structure.Map(s) +m := s.Map() // Convert the values of a struct to a []interface{} -// => [123456, "gopher", true] -v := structure.Values(s) +// => ["gopher", 123456, true] +v := s.Values() -// Convert the fields of a struct to a []string. -// => ["Name", "ID", "Enabled"] -f := structure.Fields(s) - -// Return the struct name -// => "Server" -n := structure.Name(s) - -// Check if field name exists -// => true -h := structure.Has(s, "Enabled") +// Convert the values of a struct to a []*Field +// (see "Field methods" for more info about fields) +f := s.Fields() // Check if any field of a struct is initialized or not. -if structure.HasZero(s) { +if s.HasZero() { fmt.Println("s has a zero value field") } // Check if all fields of a struct is initialized or not. -if structure.IsZero(s) { +if s.IsZero() { fmt.Println("all fields of s is zero value") } -// Check if it's a struct or a pointer to struct -if structure.IsStruct(s) { - fmt.Println("s is a struct") +// Return the struct name +// => "Server" +n := s.Name() +``` + +Most of the struct methods are available as global functions without the need +for a `New()` constructor: + +```go +m := structure.Map(s) // Get a map[string]interface{} +v := structure.Values(s) // Get a []interface{} +f := structure.Fields(s) // Get a []*Field +n := structure.Name(s) // Get the struct name +h := structure.HasZero(s) // Check if any field is initialized +z := structure.IsZero(s) // Check if all fields are initialized +i := structure.IsStruct(s) // Check if s is a struct or a pointer to struct +``` + +### Field methods + +We can easily examine a single Field for more detail. Below you can see how we +get and interact with various field methods: + + +```go +s := structure.New(server) + +// Get the Field struct for the "Name" field +name := s.Field("Name") + +// Get the underlying value, value => "gopher" +value := name.Value().(string) + +// Check if the field is exported or not +if name.IsExported() { + fmt.Println("Name field is exported") } +// Check if the value is a zero value, such as "" for string, 0 for int +if !name.IsZero() { + fmt.Println("Name is initialized") +} + +// Check if the field is an anonymous (embedded) field +if !name.IsEmbedded() { + fmt.Println("Name is not an embedded field") +} + +// Get the Field's tag value for tag name "json", tag value => "name,omitempty" +tagValue := name.Tag("json") +``` + +Nested structs are supported too: + +```go +addrField := s.Field("Server").Field("Addr") + +// Get the value for addr +a := addrField.Value().(string) +``` + +We can also get a slice of Fields from the Struct type to iterate over all +fields. This is handy if you whish to examine all fields: + +```go +// Convert the fields of a struct to a []*Field +fields := s.Fields() + +for _, f := range fields { + fmt.Printf("field name: %+v\n", f.Name()) + + if f.IsExported() { + fmt.Printf("value : %+v\n", f.Value()) + fmt.Printf("is zero : %+v\n", f.IsZero()) + } +} ``` ## Credits diff --git a/field.go b/field.go new file mode 100644 index 0000000..358a071 --- /dev/null +++ b/field.go @@ -0,0 +1,75 @@ +package structure + +import "reflect" + +// Field represents a single struct field that encapsulates high level +// functions around the field. +type Field struct { + value reflect.Value + field reflect.StructField +} + +// Tag returns the value associated with key in the tag string. If there is no +// such key in the tag, Tag returns the empty string. +func (f *Field) Tag(key string) string { + return f.field.Tag.Get(key) +} + +// Value returns the underlying value of of the field. It panics if the field +// is not exported. +func (f *Field) Value() interface{} { + return f.value.Interface() +} + +// IsEmbedded returns true if the given field is an anonymous field (embedded) +func (f *Field) IsEmbedded() bool { + return f.field.Anonymous +} + +// IsExported returns true if the given field is exported. +func (f *Field) IsExported() bool { + return f.field.PkgPath == "" +} + +// IsZero returns true if the given field is not initalized (has a zero value). +// It panics if the field is not exported. +func (f *Field) IsZero() bool { + zero := reflect.Zero(f.value.Type()).Interface() + current := f.Value() + + return reflect.DeepEqual(current, zero) +} + +// Name returns the name of the given field +func (f *Field) Name() string { + return f.field.Name +} + +// Field returns the field from a nested struct. It panics if the nested struct +// is not exported or if the field was not found. +func (f *Field) Field(name string) *Field { + field, ok := f.FieldOk(name) + if !ok { + panic("field not found") + } + + return field +} + +// Field returns the field from a nested struct. The boolean returns true if +// the field was not found. It panics if the nested struct is not exported or +// if the field was not found. +func (f *Field) FieldOk(name string) (*Field, bool) { + v := strctVal(f.value.Interface()) + t := v.Type() + + field, ok := t.FieldByName(name) + if !ok { + return nil, false + } + + return &Field{ + field: field, + value: v.FieldByName(name), + }, true +} diff --git a/field_test.go b/field_test.go new file mode 100644 index 0000000..beb892d --- /dev/null +++ b/field_test.go @@ -0,0 +1,198 @@ +package structure + +import "testing" + +// A test struct that defines all cases +type Foo struct { + A string + B int `structure:"y"` + C bool `json:"c"` + d string // not exported + x string `xml:"x"` // not exported, with tag + *Bar // embedded +} + +type Bar struct { + E string + F int + g []string +} + +func newStruct() *Struct { + b := &Bar{ + E: "example", + F: 2, + g: []string{"zeynep", "fatih"}, + } + + // B and x is not initialized for testing + f := &Foo{ + A: "gopher", + C: true, + d: "small", + } + f.Bar = b + + return New(f) +} + +func TestField(t *testing.T) { + s := newStruct() + + defer func() { + err := recover() + if err == nil { + t.Error("Retrieveing a non existing field from the struct should panic") + } + }() + + _ = s.Field("no-field") +} + +func TestField_Tag(t *testing.T) { + s := newStruct() + + v := s.Field("B").Tag("json") + if v != "" { + t.Errorf("Field's tag value of a non existing tag should return empty, got: %s", v) + } + + v = s.Field("C").Tag("json") + if v != "c" { + t.Errorf("Field's tag value of the existing field C should return 'c', got: %s", v) + } + + v = s.Field("d").Tag("json") + if v != "" { + t.Errorf("Field's tag value of a non exported field should return empty, got: %s", v) + } + + v = s.Field("x").Tag("xml") + if v != "x" { + t.Errorf("Field's tag value of a non exported field with a tag should return 'x', got: %s", v) + } + + v = s.Field("A").Tag("json") + if v != "" { + t.Errorf("Field's tag value of a existing field without a tag should return empty, got: %s", v) + } +} + +func TestField_Value(t *testing.T) { + s := newStruct() + + v := s.Field("A").Value() + val, ok := v.(string) + if !ok { + t.Errorf("Field's value of a A should be string") + } + + if val != "gopher" { + t.Errorf("Field's value of a existing tag should return 'gopher', got: %s", val) + } + + defer func() { + err := recover() + if err == nil { + t.Error("Value of a non exported field from the field should panic") + } + }() + + // should panic + _ = s.Field("d").Value() +} + +func TestField_IsEmbedded(t *testing.T) { + s := newStruct() + + if !s.Field("Bar").IsEmbedded() { + t.Errorf("Fields 'Bar' field is an embedded field") + } + + if s.Field("d").IsEmbedded() { + t.Errorf("Fields 'd' field is not an embedded field") + } +} + +func TestField_IsExported(t *testing.T) { + s := newStruct() + + if !s.Field("Bar").IsExported() { + t.Errorf("Fields 'Bar' field is an exported field") + } + + if !s.Field("A").IsExported() { + t.Errorf("Fields 'A' field is an exported field") + } + + if s.Field("d").IsExported() { + t.Errorf("Fields 'd' field is not an exported field") + } +} + +func TestField_IsZero(t *testing.T) { + s := newStruct() + + if s.Field("A").IsZero() { + t.Errorf("Fields 'A' field is an initialized field") + } + + if !s.Field("B").IsZero() { + t.Errorf("Fields 'B' field is not an initialized field") + } +} + +func TestField_Name(t *testing.T) { + s := newStruct() + + if s.Field("A").Name() != "A" { + t.Errorf("Fields 'A' field should have the name 'A'") + } +} + +func TestField_Field(t *testing.T) { + s := newStruct() + + e := s.Field("Bar").Field("E") + + val, ok := e.Value().(string) + if !ok { + t.Error("The value of the field 'e' inside 'Bar' struct should be string") + } + + if val != "example" { + t.Errorf("The value of 'e' should be 'example, got: %s", val) + } + + defer func() { + err := recover() + if err == nil { + t.Error("Field of a non existing nested struct should panic") + } + }() + + _ = s.Field("Bar").Field("e") +} + +func TestField_FieldOk(t *testing.T) { + s := newStruct() + + b, ok := s.FieldOk("Bar") + if !ok { + t.Error("The field 'Bar' should exists.") + } + + e, ok := b.FieldOk("E") + if !ok { + t.Error("The field 'E' should exists.") + } + + val, ok := e.Value().(string) + if !ok { + t.Error("The value of the field 'e' inside 'Bar' struct should be string") + } + + if val != "example" { + t.Errorf("The value of 'e' should be 'example, got: %s", val) + } +} diff --git a/structure.go b/structure.go index c4b32ae..617a2df 100644 --- a/structure.go +++ b/structure.go @@ -10,7 +10,23 @@ var ( DefaultTagName = "structure" // struct's field default tag name ) -// Map converts the given s struct to a map[string]interface{}, where the keys +// Struct encapsulates a struct type to provide several high level functions +// around the structure. +type Struct struct { + raw interface{} + value reflect.Value +} + +// New returns a new *Struct with the struct s. It panics if the s's kind is +// not struct. +func New(s interface{}) *Struct { + return &Struct{ + raw: s, + value: strctVal(s), + } +} + +// Map converts the given struct to a map[string]interface{}, where the keys // of the map are the field names and the values of the map the associated // values of the fields. The default key string is the struct field name but // can be changed in the struct field's tag value. The "structure" key in the @@ -32,15 +48,15 @@ var ( // 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{} { +// fields will be neglected. +func (s *Struct) Map() map[string]interface{} { out := make(map[string]interface{}) - v, fields := strctInfo(s) + fields := s.structFields() for _, field := range fields { name := field.Name - val := v.FieldByName(name) + val := s.value.FieldByName(name) var finalVal interface{} @@ -79,14 +95,14 @@ func Map(s interface{}) map[string]interface{} { // 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) +// fields will be neglected. +func (s *Struct) Values() []interface{} { + fields := s.structFields() var t []interface{} for _, field := range fields { - val := v.FieldByName(field.Name) + val := s.value.FieldByName(field.Name) _, tagOpts := parseTag(field.Tag.Get(DefaultTagName)) @@ -102,46 +118,65 @@ func Values(s interface{}) []interface{} { } return t - } -// Fields returns a slice of field names. A struct tag with the content of "-" +// Fields returns a slice of Fields. 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) +// It panics if s's kind is not struct. +func (s *Struct) Fields() []*Field { + t := s.value.Type() - var keys []string + var fields []*Field - for _, field := range fields { - val := v.FieldByName(field.Name) + for i := 0; i < t.NumField(); i++ { + field := t.Field(i) - _, 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) - } + if tag := field.Tag.Get(DefaultTagName); tag == "-" { + continue } - keys = append(keys, field.Name) + f := &Field{ + field: field, + value: s.value.FieldByName(field.Name), + } + + fields = append(fields, f) + } - return keys + return fields +} + +// Field returns a new Field struct that provides several high level functions +// around a single struct field entitiy. It panics if the field is not found. +func (s *Struct) Field(name string) *Field { + f, ok := s.FieldOk(name) + if !ok { + panic("field not found") + } + + return f +} + +// Field returns a new Field struct that provides several high level functions +// around a single struct field entitiy. The boolean returns true if the field +// was found. +func (s *Struct) FieldOk(name string) (*Field, bool) { + t := s.value.Type() + + field, ok := t.FieldByName(name) + if !ok { + return nil, false + } + + return &Field{ + field: field, + value: s.value.FieldByName(name), + }, true } // IsZero returns true if all fields in a struct is a zero value (not @@ -160,11 +195,11 @@ func Fields(s interface{}) []string { // // 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 { - v, fields := strctInfo(s) +func (s *Struct) IsZero() bool { + fields := s.structFields() for _, field := range fields { - val := v.FieldByName(field.Name) + val := s.value.FieldByName(field.Name) _, tagOpts := parseTag(field.Tag.Get(DefaultTagName)) @@ -207,11 +242,11 @@ func IsZero(s interface{}) bool { // // 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 { - v, fields := strctInfo(s) +func (s *Struct) HasZero() bool { + fields := s.structFields() for _, field := range fields { - val := v.FieldByName(field.Name) + val := s.value.FieldByName(field.Name) _, tagOpts := parseTag(field.Tag.Get(DefaultTagName)) @@ -238,61 +273,17 @@ func HasZero(s interface{}) bool { return false } -// IsStruct returns true if the given variable is a struct or a pointer to -// struct. -func IsStruct(s interface{}) bool { - t := reflect.TypeOf(s) - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - return t.Kind() == reflect.Struct +// Name returns the structs's type name within its package. For more info refer +// to Name() function. +func (s *Struct) Name() string { + return s.value.Type().Name() } -// Name returns the structs's type name within its package. It returns an -// empty string for unnamed types. It panics if s's kind is not struct. -func Name(s interface{}) string { - t := reflect.TypeOf(s) - - if t.Kind() == reflect.Ptr { - t = t.Elem() - } - - if t.Kind() != reflect.Struct { - panic("not struct") - } - - return t.Name() -} - -// Has returns true if the given field name exists for the struct s. It panic's -// if s's kind is not struct. -func Has(s interface{}, fieldName string) bool { - v, fields := strctInfo(s) - - for _, field := range fields { - val := v.FieldByName(field.Name) - - if IsStruct(val.Interface()) { - if ok := Has(val.Interface(), fieldName); ok { - return true - } - } - - if field.Name == fieldName { - return true - } - } - - return false -} - -// strctInfo returns the struct value and the exported struct fields for a -// given s struct. This is a convenient helper method to avoid duplicate code -// in some of the functions. -func strctInfo(s interface{}) (reflect.Value, []reflect.StructField) { - v := strctVal(s) - t := v.Type() +// structFields returns the exported struct fields for a given s struct. This +// is a convenient helper method to avoid duplicate code in some of the +// functions. +func (s *Struct) structFields() []reflect.StructField { + t := s.value.Type() var f []reflect.StructField @@ -311,7 +302,7 @@ func strctInfo(s interface{}) (reflect.Value, []reflect.StructField) { f = append(f, field) } - return v, f + return f } func strctVal(s interface{}) reflect.Value { @@ -328,3 +319,50 @@ func strctVal(s interface{}) reflect.Value { return v } + +// Map converts the given struct to a map[string]interface{}. For more info +// refer to Struct types Map() method. It panics if s's kind is not struct. +func Map(s interface{}) map[string]interface{} { + return New(s).Map() +} + +// Values converts the given struct to a []interface{}. For more info refer to +// Struct types Values() method. It panics if s's kind is not struct. +func Values(s interface{}) []interface{} { + return New(s).Values() +} + +// Fields returns a slice of *Field. For more info refer to Struct types +// Fields() method. It panics if s's kind is not struct. +func Fields(s interface{}) []*Field { + return New(s).Fields() +} + +// IsZero returns true if all fields is equal to a zero value. For more info +// refer to Struct types IsZero() method. It panics if s's kind is not struct. +func IsZero(s interface{}) bool { + return New(s).IsZero() +} + +// HasZero returns true if any field is equal to a zero value. For more info +// refer to Struct types HasZero() method. It panics if s's kind is not struct. +func HasZero(s interface{}) bool { + return New(s).HasZero() +} + +// IsStruct returns true if the given variable is a struct or a pointer to +// struct. +func IsStruct(s interface{}) bool { + t := reflect.TypeOf(s) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + + return t.Kind() == reflect.Struct +} + +// Name returns the structs's type name within its package. It returns an +// empty string for unnamed types. It panics if s's kind is not struct. +func Name(s interface{}) string { + return New(s).Name() +} diff --git a/structure_example_test.go b/structure_example_test.go index 91ca3da..18d29cc 100644 --- a/structure_example_test.go +++ b/structure_example_test.go @@ -5,6 +5,31 @@ import ( "time" ) +func ExampleNew() { + type Server struct { + Name string + ID int32 + Enabled bool + } + + server := &Server{ + Name: "Arslan", + ID: 123456, + Enabled: true, + } + + s := New(server) + + fmt.Printf("Name : %v\n", s.Name()) + fmt.Printf("Values : %v\n", s.Values()) + fmt.Printf("Value of ID : %v\n", s.Field("ID").Value()) + // Output: + // Name : Server + // Values : [Arslan 123456 true] + // Value of ID : 123456 + +} + func ExampleMap() { type Server struct { Name string @@ -148,11 +173,16 @@ func ExampleFields() { Number: 1234567, } - m := Fields(s) + fields := Fields(s) + + for i, field := range fields { + fmt.Printf("[%d] %+v\n", i, field.Name()) + } - fmt.Printf("Fields: %+v\n", m) // Output: - // Fields: [Name LastAccessed Number] + // [0] Name + // [1] LastAccessed + // [2] Number } func ExampleFields_nested() { @@ -162,7 +192,7 @@ func ExampleFields_nested() { } type Access struct { - Person Person `structure:",omitnested"` + Person Person HasPermission bool LastAccessed time.Time } @@ -173,13 +203,51 @@ func ExampleFields_nested() { 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) + // Let's get all fields from the struct s. + fields := Fields(s) + + for _, field := range fields { + if field.Name() == "Person" { + fmt.Printf("Access.Person.Name: %+v\n", field.Field("Name").Value()) + } + } - fmt.Printf("Fields: %+v\n", m) // Output: - // Fields: [Person HasPermission LastAccessed] + // Access.Person.Name: fatih +} + +func ExampleField() { + type Person struct { + Name string + Number int + } + + type Access struct { + Person Person + HasPermission bool + LastAccessed time.Time + } + + access := &Access{ + Person: Person{Name: "fatih", Number: 1234567}, + LastAccessed: time.Now(), + HasPermission: true, + } + + // Create a new Struct type + s := New(access) + + // Get the Field type for "Person" field + p := s.Field("Person") + + // Get the underlying "Name field" and print the value of it + name := p.Field("Name") + + fmt.Printf("Value of Person.Access.Name: %+v\n", name.Value()) + + // Output: + // Value of Person.Access.Name: fatih + } func ExampleIsZero() { @@ -237,23 +305,3 @@ func ExampleHasZero() { // true // false } - -func ExampleHas() { - type Access struct { - Name string - LastAccessed time.Time - Number int - } - - s := &Access{ - Name: "Fatih", - LastAccessed: time.Now(), - Number: 1234567, - } - - found := Has(s, "LastAccessed") - - fmt.Printf("Has: %+v\n", found) - // Output: - // Has: true -} diff --git a/structure_test.go b/structure_test.go index 4684f49..342cf71 100644 --- a/structure_test.go +++ b/structure_test.go @@ -1,6 +1,7 @@ package structure import ( + "fmt" "reflect" "testing" "time" @@ -29,6 +30,7 @@ func TestStructIndexes(t *testing.T) { defer func() { err := recover() if err != nil { + fmt.Printf("err %+v\n", err) t.Error("Using mixed indexes should not panic") } }() @@ -397,7 +399,7 @@ func TestFields(t *testing.T) { inSlice := func(val string) bool { for _, v := range s { - if reflect.DeepEqual(v, val) { + if reflect.DeepEqual(v.Name(), val) { return true } } @@ -414,27 +416,27 @@ 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 + A A + C int + Value string `structure:"-"` + Number int } b := &B{A: a, C: 123} s := Fields(b) - if len(s) != 2 { + if len(s) != 3 { 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) { + if reflect.DeepEqual(v.Name(), val) { return true } } @@ -448,36 +450,6 @@ func TestFields_OmitNested(t *testing.T) { } } -func TestFields_Nested(t *testing.T) { - type A struct { - Name string - } - a := A{Name: "example"} - - type B struct { - A A - C int - } - b := &B{A: a, C: 123} - - s := Fields(b) - - inSlice := func(val interface{}) bool { - for _, v := range s { - if reflect.DeepEqual(v, val) { - return true - } - } - return false - } - - for _, val := range []interface{}{"Name", "A", "C"} { - if !inSlice(val) { - t.Errorf("Fields should have the value %v", val) - } - } -} - func TestFields_Anonymous(t *testing.T) { type A struct { Name string @@ -495,14 +467,14 @@ func TestFields_Anonymous(t *testing.T) { inSlice := func(val interface{}) bool { for _, v := range s { - if reflect.DeepEqual(v, val) { + if reflect.DeepEqual(v.Name(), val) { return true } } return false } - for _, val := range []interface{}{"Name", "A", "C"} { + for _, val := range []interface{}{"A", "C"} { if !inSlice(val) { t.Errorf("Fields should have the value %v", val) } @@ -735,33 +707,6 @@ func TestHasZero_Anonymous(t *testing.T) { } } -func TestHas(t *testing.T) { - type A struct { - Name string - D string - } - a := A{Name: "example"} - - type B struct { - A - C int - } - b := &B{C: 123} - b.A = a - - if !Has(b, "Name") { - t.Error("Has should return true for Name, but it's false") - } - - if Has(b, "NotAvailable") { - t.Error("Has should return false for NotAvailable, but it's true") - } - - if !Has(b, "C") { - t.Error("Has should return true for C, but it's false") - } -} - func TestName(t *testing.T) { type Foo struct { A string