mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
187 lines
5.5 KiB
187 lines
5.5 KiB
/* |
|
Package decode provides tools for customizing the decoding of configuration, |
|
into structures using mapstructure. |
|
*/ |
|
package decode |
|
|
|
import ( |
|
"reflect" |
|
"strings" |
|
|
|
"github.com/mitchellh/reflectwalk" |
|
) |
|
|
|
// HookTranslateKeys is a mapstructure decode hook which translates keys in a |
|
// map to their canonical value. |
|
// |
|
// Any struct field with a field tag of `alias` may be loaded from any of the |
|
// values keyed by any of the aliases. A field may have one or more alias. |
|
// Aliases must be lowercase, as keys are compared case-insensitive. |
|
// |
|
// Example alias tag: |
|
// MyField []string `alias:"old_field_name,otherfieldname"` |
|
// |
|
// This hook should ONLY be used to maintain backwards compatibility with |
|
// deprecated keys. For new structures use mapstructure struct tags to set the |
|
// desired serialization key. |
|
// |
|
// IMPORTANT: This function assumes that mapstructure is being used with the |
|
// default struct field tag of `mapstructure`. If mapstructure.DecoderConfig.TagName |
|
// is set to a different value this function will need to be parameterized with |
|
// that value to correctly find the canonical data key. |
|
func HookTranslateKeys(_, to reflect.Type, data interface{}) (interface{}, error) { |
|
// Return immediately if target is not a struct, as only structs can have |
|
// field tags. If the target is a pointer to a struct, mapstructure will call |
|
// the hook again with the struct. |
|
if to.Kind() != reflect.Struct { |
|
return data, nil |
|
} |
|
|
|
// Avoid doing any work if data is not a map |
|
source, ok := data.(map[string]interface{}) |
|
if !ok { |
|
return data, nil |
|
} |
|
|
|
rules := translationsForType(to) |
|
// Avoid making a copy if there are no translation rules |
|
if len(rules) == 0 { |
|
return data, nil |
|
} |
|
result := make(map[string]interface{}, len(source)) |
|
for k, v := range source { |
|
lowerK := strings.ToLower(k) |
|
canonKey, ok := rules[lowerK] |
|
if !ok { |
|
result[k] = v |
|
continue |
|
} |
|
|
|
// if there is a value for the canonical key then keep it |
|
if canonValue, ok := source[canonKey]; ok { |
|
// Assign the value for the case where canonKey == k |
|
result[canonKey] = canonValue |
|
continue |
|
} |
|
result[canonKey] = v |
|
} |
|
return result, nil |
|
} |
|
|
|
// TODO: could be cached if it is too slow |
|
func translationsForType(to reflect.Type) map[string]string { |
|
translations := map[string]string{} |
|
for i := 0; i < to.NumField(); i++ { |
|
field := to.Field(i) |
|
tag, ok := field.Tag.Lookup("alias") |
|
if !ok { |
|
continue |
|
} |
|
|
|
canonKey := strings.ToLower(canonicalFieldKey(field)) |
|
for _, alias := range strings.Split(tag, ",") { |
|
translations[strings.ToLower(alias)] = canonKey |
|
} |
|
} |
|
return translations |
|
} |
|
|
|
func canonicalFieldKey(field reflect.StructField) string { |
|
tag, ok := field.Tag.Lookup("mapstructure") |
|
if !ok { |
|
return field.Name |
|
} |
|
parts := strings.SplitN(tag, ",", 2) |
|
switch { |
|
case len(parts) < 1: |
|
return field.Name |
|
case parts[0] == "": |
|
return field.Name |
|
} |
|
return parts[0] |
|
} |
|
|
|
// HookWeakDecodeFromSlice looks for []map[string]interface{} and []interface{} |
|
// in the source data. If the target is not a slice or array it attempts to unpack |
|
// 1 item out of the slice. If there are more items the source data is left |
|
// unmodified, allowing mapstructure to handle and report the decode error caused by |
|
// mismatched types. The []interface{} is handled so that all slice types are |
|
// behave the same way, and for the rare case when a raw structure is re-encoded |
|
// to JSON, which will produce the []interface{}. |
|
// |
|
// If this hook is being used on a "second pass" decode to decode an opaque |
|
// configuration into a type, the DecodeConfig should set WeaklyTypedInput=true, |
|
// (or another hook) to convert any scalar values into a slice of one value when |
|
// the target is a slice. This is necessary because this hook would have converted |
|
// the initial slices into single values on the first pass. |
|
// |
|
// Background |
|
// |
|
// HCL allows for repeated blocks which forces it to store structures |
|
// as []map[string]interface{} instead of map[string]interface{}. This is an |
|
// ambiguity which makes the generated structures incompatible with the |
|
// corresponding JSON data. |
|
// |
|
// This hook allows config to be read from the HCL format into a raw structure, |
|
// and later decoded into a strongly typed structure. |
|
func HookWeakDecodeFromSlice(from, to reflect.Type, data interface{}) (interface{}, error) { |
|
if from.Kind() == reflect.Slice && (to.Kind() == reflect.Slice || to.Kind() == reflect.Array) { |
|
return data, nil |
|
} |
|
|
|
switch d := data.(type) { |
|
case []map[string]interface{}: |
|
switch { |
|
case len(d) != 1: |
|
return data, nil |
|
case to == typeOfEmptyInterface: |
|
return unSlice(d[0]) |
|
default: |
|
return d[0], nil |
|
} |
|
|
|
// a slice map be encoded as []interface{} in some cases |
|
case []interface{}: |
|
switch { |
|
case len(d) != 1: |
|
return data, nil |
|
case to == typeOfEmptyInterface: |
|
return unSlice(d[0]) |
|
default: |
|
return d[0], nil |
|
} |
|
} |
|
return data, nil |
|
} |
|
|
|
var typeOfEmptyInterface = reflect.TypeOf((*interface{})(nil)).Elem() |
|
|
|
func unSlice(data interface{}) (interface{}, error) { |
|
err := reflectwalk.Walk(data, &unSliceWalker{}) |
|
return data, err |
|
} |
|
|
|
type unSliceWalker struct{} |
|
|
|
func (u *unSliceWalker) Map(_ reflect.Value) error { |
|
return nil |
|
} |
|
|
|
func (u *unSliceWalker) MapElem(m, k, v reflect.Value) error { |
|
if !v.IsValid() || v.Kind() != reflect.Interface { |
|
return nil |
|
} |
|
|
|
v = v.Elem() // unpack the value from the interface{} |
|
if v.Kind() != reflect.Slice || v.Len() != 1 { |
|
return nil |
|
} |
|
|
|
first := v.Index(0) |
|
// The value should always be assignable, but double check to avoid a panic. |
|
if !first.Type().AssignableTo(m.Type().Elem()) { |
|
return nil |
|
} |
|
m.SetMapIndex(k, first) |
|
return nil |
|
}
|
|
|