585 lines
15 KiB
Go
585 lines
15 KiB
Go
|
// Package structs contains various utilities functions to work with structs.
|
||
|
package structs
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
|
||
|
"reflect"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
// DefaultTagName is the default tag name for struct fields which provides
|
||
|
// a more granular to tweak certain structs. Lookup the necessary functions
|
||
|
// for more info.
|
||
|
DefaultTagName = "structs" // struct's field default tag name
|
||
|
)
|
||
|
|
||
|
// Struct encapsulates a struct type to provide several high level functions
|
||
|
// around the struct.
|
||
|
type Struct struct {
|
||
|
raw interface{}
|
||
|
value reflect.Value
|
||
|
TagName string
|
||
|
}
|
||
|
|
||
|
// 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),
|
||
|
TagName: DefaultTagName,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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 "structs" key in the
|
||
|
// struct's field tag value is the key name. Example:
|
||
|
//
|
||
|
// // Field appears in map as key "myName".
|
||
|
// Name string `structs:"myName"`
|
||
|
//
|
||
|
// A tag value with the content of "-" ignores that particular field. Example:
|
||
|
//
|
||
|
// // Field is ignored by this package.
|
||
|
// Field bool `structs:"-"`
|
||
|
//
|
||
|
// A tag value with the content of "string" uses the stringer to get the value. Example:
|
||
|
//
|
||
|
// // The value will be output of Animal's String() func.
|
||
|
// // Map will panic if Animal does not implement String().
|
||
|
// Field *Animal `structs:"field,string"`
|
||
|
//
|
||
|
// A tag value with the option of "flatten" used in a struct field is to flatten its fields
|
||
|
// in the output map. Example:
|
||
|
//
|
||
|
// // The FieldStruct's fields will be flattened into the output map.
|
||
|
// FieldStruct time.Time `structs:",flatten"`
|
||
|
//
|
||
|
// A tag 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 `structs:"myName,omitnested"`
|
||
|
// Field *http.Request `structs:",omitnested"`
|
||
|
//
|
||
|
// A tag value with the option of "omitempty" ignores that particular field if
|
||
|
// the field value is empty. Example:
|
||
|
//
|
||
|
// // Field appears in map as key "myName", but the field is
|
||
|
// // skipped if empty.
|
||
|
// Field string `structs:"myName,omitempty"`
|
||
|
//
|
||
|
// // Field appears in map as key "Field" (the default), but
|
||
|
// // the field is skipped if empty.
|
||
|
// Field string `structs:",omitempty"`
|
||
|
//
|
||
|
// Note that only exported fields of a struct can be accessed, non exported
|
||
|
// fields will be neglected.
|
||
|
func (s *Struct) Map() map[string]interface{} {
|
||
|
out := make(map[string]interface{})
|
||
|
s.FillMap(out)
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
// FillMap is the same as Map. Instead of returning the output, it fills the
|
||
|
// given map.
|
||
|
func (s *Struct) FillMap(out map[string]interface{}) {
|
||
|
if out == nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
fields := s.structFields()
|
||
|
|
||
|
for _, field := range fields {
|
||
|
name := field.Name
|
||
|
val := s.value.FieldByName(name)
|
||
|
isSubStruct := false
|
||
|
var finalVal interface{}
|
||
|
|
||
|
tagName, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||
|
if tagName != "" {
|
||
|
name = tagName
|
||
|
}
|
||
|
|
||
|
// if the value is a zero value and the field is marked as omitempty do
|
||
|
// not include
|
||
|
if tagOpts.Has("omitempty") {
|
||
|
zero := reflect.Zero(val.Type()).Interface()
|
||
|
current := val.Interface()
|
||
|
|
||
|
if reflect.DeepEqual(current, zero) {
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if !tagOpts.Has("omitnested") {
|
||
|
finalVal = s.nested(val)
|
||
|
|
||
|
v := reflect.ValueOf(val.Interface())
|
||
|
if v.Kind() == reflect.Ptr {
|
||
|
v = v.Elem()
|
||
|
}
|
||
|
|
||
|
switch v.Kind() {
|
||
|
case reflect.Map, reflect.Struct:
|
||
|
isSubStruct = true
|
||
|
}
|
||
|
} else {
|
||
|
finalVal = val.Interface()
|
||
|
}
|
||
|
|
||
|
if tagOpts.Has("string") {
|
||
|
s, ok := val.Interface().(fmt.Stringer)
|
||
|
if ok {
|
||
|
out[name] = s.String()
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if isSubStruct && (tagOpts.Has("flatten")) {
|
||
|
for k := range finalVal.(map[string]interface{}) {
|
||
|
out[k] = finalVal.(map[string]interface{})[k]
|
||
|
}
|
||
|
} else {
|
||
|
out[name] = finalVal
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Values converts the given s struct's field values to a []interface{}. A
|
||
|
// struct tag with the content of "-" ignores the that particular field.
|
||
|
// Example:
|
||
|
//
|
||
|
// // Field is ignored by this package.
|
||
|
// Field int `structs:"-"`
|
||
|
//
|
||
|
// A value with the option of "omitnested" stops iterating further if the type
|
||
|
// is a struct. Example:
|
||
|
//
|
||
|
// // Fields is not processed further by this package.
|
||
|
// Field time.Time `structs:",omitnested"`
|
||
|
// Field *http.Request `structs:",omitnested"`
|
||
|
//
|
||
|
// A tag value with the option of "omitempty" ignores that particular field and
|
||
|
// is not added to the values if the field value is empty. Example:
|
||
|
//
|
||
|
// // Field is skipped if empty
|
||
|
// Field string `structs:",omitempty"`
|
||
|
//
|
||
|
// Note that only exported fields of a struct can be accessed, non exported
|
||
|
// fields will be neglected.
|
||
|
func (s *Struct) Values() []interface{} {
|
||
|
fields := s.structFields()
|
||
|
|
||
|
var t []interface{}
|
||
|
|
||
|
for _, field := range fields {
|
||
|
val := s.value.FieldByName(field.Name)
|
||
|
|
||
|
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||
|
|
||
|
// if the value is a zero value and the field is marked as omitempty do
|
||
|
// not include
|
||
|
if tagOpts.Has("omitempty") {
|
||
|
zero := reflect.Zero(val.Type()).Interface()
|
||
|
current := val.Interface()
|
||
|
|
||
|
if reflect.DeepEqual(current, zero) {
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if tagOpts.Has("string") {
|
||
|
s, ok := val.Interface().(fmt.Stringer)
|
||
|
if ok {
|
||
|
t = append(t, s.String())
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
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
|
||
|
t = append(t, Values(val.Interface())...)
|
||
|
} else {
|
||
|
t = append(t, val.Interface())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return t
|
||
|
}
|
||
|
|
||
|
// 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 `structs:"-"`
|
||
|
//
|
||
|
// It panics if s's kind is not struct.
|
||
|
func (s *Struct) Fields() []*Field {
|
||
|
return getFields(s.value, s.TagName)
|
||
|
}
|
||
|
|
||
|
// Names 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 `structs:"-"`
|
||
|
//
|
||
|
// It panics if s's kind is not struct.
|
||
|
func (s *Struct) Names() []string {
|
||
|
fields := getFields(s.value, s.TagName)
|
||
|
|
||
|
names := make([]string, len(fields))
|
||
|
|
||
|
for i, field := range fields {
|
||
|
names[i] = field.Name()
|
||
|
}
|
||
|
|
||
|
return names
|
||
|
}
|
||
|
|
||
|
func getFields(v reflect.Value, tagName string) []*Field {
|
||
|
if v.Kind() == reflect.Ptr {
|
||
|
v = v.Elem()
|
||
|
}
|
||
|
|
||
|
t := v.Type()
|
||
|
|
||
|
var fields []*Field
|
||
|
|
||
|
for i := 0; i < t.NumField(); i++ {
|
||
|
field := t.Field(i)
|
||
|
|
||
|
if tag := field.Tag.Get(tagName); tag == "-" {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
f := &Field{
|
||
|
field: field,
|
||
|
value: v.FieldByName(field.Name),
|
||
|
}
|
||
|
|
||
|
fields = append(fields, f)
|
||
|
|
||
|
}
|
||
|
|
||
|
return fields
|
||
|
}
|
||
|
|
||
|
// Field returns a new Field struct that provides several high level functions
|
||
|
// around a single struct field entity. 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
|
||
|
}
|
||
|
|
||
|
// FieldOk returns a new Field struct that provides several high level functions
|
||
|
// around a single struct field entity. 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),
|
||
|
defaultTag: s.TagName,
|
||
|
}, true
|
||
|
}
|
||
|
|
||
|
// 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:
|
||
|
//
|
||
|
// // Field is ignored by this package.
|
||
|
// Field bool `structs:"-"`
|
||
|
//
|
||
|
// 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 `structs:"myName,omitnested"`
|
||
|
// Field *http.Request `structs:",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 (s *Struct) IsZero() bool {
|
||
|
fields := s.structFields()
|
||
|
|
||
|
for _, field := range fields {
|
||
|
val := s.value.FieldByName(field.Name)
|
||
|
|
||
|
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||
|
|
||
|
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
|
||
|
ok := IsZero(val.Interface())
|
||
|
if !ok {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// zero value of the given field, such as "" for string, 0 for int
|
||
|
zero := reflect.Zero(val.Type()).Interface()
|
||
|
|
||
|
// current value of the given field
|
||
|
current := val.Interface()
|
||
|
|
||
|
if !reflect.DeepEqual(current, zero) {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
// HasZero returns true if a field in a struct is not initialized (zero value).
|
||
|
// A struct tag with the content of "-" ignores the checking of that particular
|
||
|
// field. Example:
|
||
|
//
|
||
|
// // Field is ignored by this package.
|
||
|
// Field bool `structs:"-"`
|
||
|
//
|
||
|
// 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 `structs:"myName,omitnested"`
|
||
|
// Field *http.Request `structs:",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 (s *Struct) HasZero() bool {
|
||
|
fields := s.structFields()
|
||
|
|
||
|
for _, field := range fields {
|
||
|
val := s.value.FieldByName(field.Name)
|
||
|
|
||
|
_, tagOpts := parseTag(field.Tag.Get(s.TagName))
|
||
|
|
||
|
if IsStruct(val.Interface()) && !tagOpts.Has("omitnested") {
|
||
|
ok := HasZero(val.Interface())
|
||
|
if ok {
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// zero value of the given field, such as "" for string, 0 for int
|
||
|
zero := reflect.Zero(val.Type()).Interface()
|
||
|
|
||
|
// current value of the given field
|
||
|
current := val.Interface()
|
||
|
|
||
|
if reflect.DeepEqual(current, zero) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// 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()
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
|
||
|
for i := 0; i < t.NumField(); i++ {
|
||
|
field := t.Field(i)
|
||
|
// we can't access the value of unexported fields
|
||
|
if field.PkgPath != "" {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// don't check if it's omitted
|
||
|
if tag := field.Tag.Get(s.TagName); tag == "-" {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
f = append(f, field)
|
||
|
}
|
||
|
|
||
|
return f
|
||
|
}
|
||
|
|
||
|
func strctVal(s interface{}) reflect.Value {
|
||
|
v := reflect.ValueOf(s)
|
||
|
|
||
|
// if pointer get the underlying element≤
|
||
|
for v.Kind() == reflect.Ptr {
|
||
|
v = v.Elem()
|
||
|
}
|
||
|
|
||
|
if v.Kind() != reflect.Struct {
|
||
|
panic("not struct")
|
||
|
}
|
||
|
|
||
|
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()
|
||
|
}
|
||
|
|
||
|
// FillMap is the same as Map. Instead of returning the output, it fills the
|
||
|
// given map.
|
||
|
func FillMap(s interface{}, out map[string]interface{}) {
|
||
|
New(s).FillMap(out)
|
||
|
}
|
||
|
|
||
|
// 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()
|
||
|
}
|
||
|
|
||
|
// Names returns a slice of field names. For more info refer to Struct types
|
||
|
// Names() method. It panics if s's kind is not struct.
|
||
|
func Names(s interface{}) []string {
|
||
|
return New(s).Names()
|
||
|
}
|
||
|
|
||
|
// 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 {
|
||
|
v := reflect.ValueOf(s)
|
||
|
if v.Kind() == reflect.Ptr {
|
||
|
v = v.Elem()
|
||
|
}
|
||
|
|
||
|
// uninitialized zero value of a struct
|
||
|
if v.Kind() == reflect.Invalid {
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
return v.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()
|
||
|
}
|
||
|
|
||
|
// nested retrieves recursively all types for the given value and returns the
|
||
|
// nested value.
|
||
|
func (s *Struct) nested(val reflect.Value) interface{} {
|
||
|
var finalVal interface{}
|
||
|
|
||
|
v := reflect.ValueOf(val.Interface())
|
||
|
if v.Kind() == reflect.Ptr {
|
||
|
v = v.Elem()
|
||
|
}
|
||
|
|
||
|
switch v.Kind() {
|
||
|
case reflect.Struct:
|
||
|
n := New(val.Interface())
|
||
|
n.TagName = s.TagName
|
||
|
m := n.Map()
|
||
|
|
||
|
// do not add the converted value if there are no exported fields, ie:
|
||
|
// time.Time
|
||
|
if len(m) == 0 {
|
||
|
finalVal = val.Interface()
|
||
|
} else {
|
||
|
finalVal = m
|
||
|
}
|
||
|
case reflect.Map:
|
||
|
// get the element type of the map
|
||
|
mapElem := val.Type()
|
||
|
switch val.Type().Kind() {
|
||
|
case reflect.Ptr, reflect.Array, reflect.Map,
|
||
|
reflect.Slice, reflect.Chan:
|
||
|
mapElem = val.Type().Elem()
|
||
|
if mapElem.Kind() == reflect.Ptr {
|
||
|
mapElem = mapElem.Elem()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// only iterate over struct types, ie: map[string]StructType,
|
||
|
// map[string][]StructType,
|
||
|
if mapElem.Kind() == reflect.Struct ||
|
||
|
(mapElem.Kind() == reflect.Slice &&
|
||
|
mapElem.Elem().Kind() == reflect.Struct) {
|
||
|
m := make(map[string]interface{}, val.Len())
|
||
|
for _, k := range val.MapKeys() {
|
||
|
m[k.String()] = s.nested(val.MapIndex(k))
|
||
|
}
|
||
|
finalVal = m
|
||
|
break
|
||
|
}
|
||
|
|
||
|
// TODO(arslan): should this be optional?
|
||
|
finalVal = val.Interface()
|
||
|
case reflect.Slice, reflect.Array:
|
||
|
if val.Type().Kind() == reflect.Interface {
|
||
|
finalVal = val.Interface()
|
||
|
break
|
||
|
}
|
||
|
|
||
|
// TODO(arslan): should this be optional?
|
||
|
// do not iterate of non struct types, just pass the value. Ie: []int,
|
||
|
// []string, co... We only iterate further if it's a struct.
|
||
|
// i.e []foo or []*foo
|
||
|
if val.Type().Elem().Kind() != reflect.Struct &&
|
||
|
!(val.Type().Elem().Kind() == reflect.Ptr &&
|
||
|
val.Type().Elem().Elem().Kind() == reflect.Struct) {
|
||
|
finalVal = val.Interface()
|
||
|
break
|
||
|
}
|
||
|
|
||
|
slices := make([]interface{}, val.Len())
|
||
|
for x := 0; x < val.Len(); x++ {
|
||
|
slices[x] = s.nested(val.Index(x))
|
||
|
}
|
||
|
finalVal = slices
|
||
|
default:
|
||
|
finalVal = val.Interface()
|
||
|
}
|
||
|
|
||
|
return finalVal
|
||
|
}
|