resource/field_extractor.go
// Copyright © 2016 Asteris, LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package resource
import (
"reflect"
"github.com/pkg/errors"
)
// FieldMap represents a map of field names to interfaces
type FieldMap map[string]interface{}
/* Rules for field access in structures:
- Fields that are tagged with `export` will be exported.
- Named structs that are tagged with `export` will be exported as a struct
- Embedded structs will have their exported fields exported in the namespace of
the containing struct
- Embedded interfaces will not be exported, nor have their fields exported
- If an embedded struct field name collides with a field from the struct that
it's embedded in, both will be exported with the embedded struct being
accessible with 'StructName.FieldName' */
var (
// ErrNilStruct is returned if there is an attempt to introspect a nil value
ErrNilStruct = errors.New("cannot inspect types with a nil value")
// ErrNonStructIntrospection is returned when attempting to introspect fields
// on a non-struct type.
ErrNonStructIntrospection = errors.New("cannot extract fields for types other than struct")
// ErrFieldIndexOutOfBounds is returned when a field index is beyond the
// number of fields in the struct type
ErrFieldIndexOutOfBounds = errors.New("struct field index is out of bounds")
// ErrDuplicateFieldName is returned if there is a duplicate reference name in
// an exported field slice during map construction
ErrDuplicateFieldName = errors.New("detected duplicate field name")
)
// ExportedField represents an exported field, including the containing struct,
// offset, field name, and lookup name
type ExportedField struct {
FieldName string
ReferenceName string
StructField *reflect.StructField
Value reflect.Value
}
func newExportedField(input interface{}, index int) (*ExportedField, bool) {
if nil == input {
return nil, false
}
val, err := getStruct(reflect.ValueOf(input))
if err != nil {
return nil, false
}
if index >= val.Type().NumField() {
return nil, false
}
fieldType := val.Type().Field(index)
fieldVal := val.Field(index)
exportedName, ok := fieldType.Tag.Lookup("export")
if !ok {
return nil, false
}
return &ExportedField{
FieldName: fieldType.Name,
ReferenceName: exportedName,
Value: fieldVal,
}, true
}
func isExportedField(input interface{}, index int) bool {
if nil == input {
return false
}
val, err := getStruct(reflect.ValueOf(input))
if err != nil {
return false
}
if index >= val.Type().NumField() {
return false
}
fieldType := val.Type().Field(index)
if _, ok := fieldType.Tag.Lookup("export"); ok {
return true
}
if _, ok := fieldType.Tag.Lookup("re-export-as"); ok {
return true
}
return false
}
// ExportedFields returns a slice of fields that have been exported from a
// struct; including embedded fields
func ExportedFields(input interface{}) (exported []*ExportedField, err error) {
nonEmbeddedFields := make(map[string]struct{})
embeddedFields := make(map[string][]*ExportedField)
if nil == input {
return exported, ErrNilStruct
}
asStruct, err := getStruct(reflect.ValueOf(input))
if err != nil {
return exported, err
}
for i := 0; i < asStruct.Type().NumField(); i++ {
isAnon, anonErr := fieldIsAnonymous(input, i)
if !isExportedField(input, i) && !isAnon {
continue
}
if anonErr != nil {
return exported, anonErr
}
if isAnon {
isKind, kindErr := fieldIsKind(input, i, reflect.Struct)
if kindErr != nil {
return exported, kindErr
}
if !isKind {
continue
}
thisField := asStruct.Field(i).Interface()
fromEmbedded, err := ExportedFields(thisField)
if err != nil {
return exported, err
}
embeddedFields[asStruct.Type().Field(i).Name] = fromEmbedded
continue
}
exportedAs, isReExported := asStruct.Type().Field(i).Tag.Lookup("re-export-as")
if isReExported {
if !asStruct.Field(i).IsValid() {
continue
}
if k := asStruct.Field(i).Kind(); k == reflect.Ptr || k == reflect.Interface {
if asStruct.Field(i).IsNil() {
continue
}
}
val, err := getStruct(asStruct.Field(i))
if err != nil {
return exported, err
}
thisField := val.Interface()
fromEmbedded, err := ExportedFields(thisField)
if err != nil {
return exported, err
}
for _, f := range fromEmbedded {
f.ReferenceName = exportedAs + "." + f.ReferenceName
exported = append(exported, f)
}
continue
}
if field, ok := newExportedField(input, i); ok {
nonEmbeddedFields[field.ReferenceName] = struct{}{}
exported = append(exported, field)
}
}
for embeddedStruct, fieldSet := range embeddedFields {
exported = append(exported, disambiguateFields(nonEmbeddedFields, embeddedStruct, fieldSet)...)
}
return exported, nil
}
// disambiguateFields will prefix the struct name to the exported field name for
// any exported field whos name would collide with the exported fields of the
// parent struct
func disambiguateFields(
structFields map[string]struct{},
structName string,
fields []*ExportedField,
) []*ExportedField {
for _, field := range fields {
if _, ok := structFields[field.ReferenceName]; ok {
field.ReferenceName = structName + "." + field.ReferenceName
}
}
return fields
}
// GenerateLookupMap takes an exported field list and generates a map of lookup
// names to values
func GenerateLookupMap(fields []*ExportedField) (FieldMap, error) {
output := make(FieldMap)
for _, field := range fields {
_, ok := output[field.ReferenceName]
if ok {
return output, errors.Wrap(ErrDuplicateFieldName, field.ReferenceName)
}
output[field.ReferenceName] = field.Value.Interface()
}
return output, nil
}
// LookupMapFromStruct generates a lookup map from a struct
func LookupMapFromStruct(input interface{}) (FieldMap, error) {
exported, err := ExportedFields(input)
if err != nil {
return make(FieldMap), err
}
return GenerateLookupMap(exported)
}
// LookupMapFromInterface gets the concrete implementation of an interface and
// then gets the struct fields from it
func LookupMapFromInterface(input interface{}) (FieldMap, error) {
switch reflect.ValueOf(input).Kind() {
case reflect.Ptr, reflect.Interface:
return LookupMapFromInterface(reflect.ValueOf(input).Elem().Interface())
case reflect.Struct:
return LookupMapFromStruct(input)
}
return make(FieldMap), ErrNonStructIntrospection
}
func getFieldKind(input interface{}, index int) (reflect.Kind, error) {
if input == nil {
return reflect.Invalid, ErrNilStruct
}
asStruct, err := getStruct(reflect.ValueOf(input))
if err != nil {
return reflect.Invalid, err
}
if index >= asStruct.Type().NumField() {
return reflect.Invalid, ErrFieldIndexOutOfBounds
}
fieldType := asStruct.Type().Field(index).Type
for fieldType.Kind() == reflect.Ptr {
fieldType = fieldType.Elem()
}
return fieldType.Kind(), nil
}
func fieldIsKind(input interface{}, index int, kinds ...reflect.Kind) (bool, error) {
for _, kind := range kinds {
actualKind, err := getFieldKind(input, index)
if err != nil {
return false, err
}
if kind == actualKind {
return true, nil
}
}
return false, nil
}
func fieldIsAnonymous(input interface{}, index int) (bool, error) {
if input == nil {
return false, ErrNilStruct
}
asStruct, err := getStruct(reflect.ValueOf(input))
if err != nil {
return false, err
}
if index >= asStruct.Type().NumField() {
return false, ErrFieldIndexOutOfBounds
}
return asStruct.Type().Field(index).Anonymous, nil
}
func getStruct(val reflect.Value) (reflect.Value, error) {
for val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return val, ErrNonStructIntrospection
}
return val, nil
}
func interfaceToConcreteType(i interface{}) reflect.Type {
var t reflect.Type
switch typed := i.(type) {
case reflect.Type:
t = typed
case reflect.Value:
t = typed.Type()
default:
t = reflect.TypeOf(i)
}
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t
}