/ *
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
}