Merge pull request #11 from fatih/struct

struct and field: two new types for a more advanced struct controlling
This commit is contained in:
Fatih Arslan 2014-08-11 04:22:45 +03:00
commit 5e0c20298b
6 changed files with 589 additions and 213 deletions

118
README.md
View File

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

75
field.go Normal file
View File

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

198
field_test.go Normal file
View File

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

View File

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

View File

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

View File

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