github.com/AliyunContainerService/cli@v0.0.0-20181009023821-814ced4b30d0/cli/compose/interpolation/interpolation.go (about) 1 package interpolation 2 3 import ( 4 "os" 5 "strings" 6 7 "github.com/docker/cli/cli/compose/template" 8 "github.com/pkg/errors" 9 ) 10 11 // Options supported by Interpolate 12 type Options struct { 13 // LookupValue from a key 14 LookupValue LookupValue 15 // TypeCastMapping maps key paths to functions to cast to a type 16 TypeCastMapping map[Path]Cast 17 // Substitution function to use 18 Substitute func(string, template.Mapping) (string, error) 19 } 20 21 // LookupValue is a function which maps from variable names to values. 22 // Returns the value as a string and a bool indicating whether 23 // the value is present, to distinguish between an empty string 24 // and the absence of a value. 25 type LookupValue func(key string) (string, bool) 26 27 // Cast a value to a new type, or return an error if the value can't be cast 28 type Cast func(value string) (interface{}, error) 29 30 // Interpolate replaces variables in a string with the values from a mapping 31 func Interpolate(config map[string]interface{}, opts Options) (map[string]interface{}, error) { 32 if opts.LookupValue == nil { 33 opts.LookupValue = os.LookupEnv 34 } 35 if opts.TypeCastMapping == nil { 36 opts.TypeCastMapping = make(map[Path]Cast) 37 } 38 if opts.Substitute == nil { 39 opts.Substitute = template.Substitute 40 } 41 42 out := map[string]interface{}{} 43 44 for key, value := range config { 45 interpolatedValue, err := recursiveInterpolate(value, NewPath(key), opts) 46 if err != nil { 47 return out, err 48 } 49 out[key] = interpolatedValue 50 } 51 52 return out, nil 53 } 54 55 func recursiveInterpolate(value interface{}, path Path, opts Options) (interface{}, error) { 56 switch value := value.(type) { 57 58 case string: 59 newValue, err := opts.Substitute(value, template.Mapping(opts.LookupValue)) 60 if err != nil || newValue == value { 61 return value, newPathError(path, err) 62 } 63 caster, ok := opts.getCasterForPath(path) 64 if !ok { 65 return newValue, nil 66 } 67 casted, err := caster(newValue) 68 return casted, newPathError(path, errors.Wrap(err, "failed to cast to expected type")) 69 70 case map[string]interface{}: 71 out := map[string]interface{}{} 72 for key, elem := range value { 73 interpolatedElem, err := recursiveInterpolate(elem, path.Next(key), opts) 74 if err != nil { 75 return nil, err 76 } 77 out[key] = interpolatedElem 78 } 79 return out, nil 80 81 case []interface{}: 82 out := make([]interface{}, len(value)) 83 for i, elem := range value { 84 interpolatedElem, err := recursiveInterpolate(elem, path.Next(PathMatchList), opts) 85 if err != nil { 86 return nil, err 87 } 88 out[i] = interpolatedElem 89 } 90 return out, nil 91 92 default: 93 return value, nil 94 95 } 96 } 97 98 func newPathError(path Path, err error) error { 99 switch err := err.(type) { 100 case nil: 101 return nil 102 case *template.InvalidTemplateError: 103 return errors.Errorf( 104 "invalid interpolation format for %s: %#v. You may need to escape any $ with another $.", 105 path, err.Template) 106 default: 107 return errors.Wrapf(err, "error while interpolating %s", path) 108 } 109 } 110 111 const pathSeparator = "." 112 113 // PathMatchAll is a token used as part of a Path to match any key at that level 114 // in the nested structure 115 const PathMatchAll = "*" 116 117 // PathMatchList is a token used as part of a Path to match items in a list 118 const PathMatchList = "[]" 119 120 // Path is a dotted path of keys to a value in a nested mapping structure. A * 121 // section in a path will match any key in the mapping structure. 122 type Path string 123 124 // NewPath returns a new Path 125 func NewPath(items ...string) Path { 126 return Path(strings.Join(items, pathSeparator)) 127 } 128 129 // Next returns a new path by append part to the current path 130 func (p Path) Next(part string) Path { 131 return Path(string(p) + pathSeparator + part) 132 } 133 134 func (p Path) parts() []string { 135 return strings.Split(string(p), pathSeparator) 136 } 137 138 func (p Path) matches(pattern Path) bool { 139 patternParts := pattern.parts() 140 parts := p.parts() 141 142 if len(patternParts) != len(parts) { 143 return false 144 } 145 for index, part := range parts { 146 switch patternParts[index] { 147 case PathMatchAll, part: 148 continue 149 default: 150 return false 151 } 152 } 153 return true 154 } 155 156 func (o Options) getCasterForPath(path Path) (Cast, bool) { 157 for pattern, caster := range o.TypeCastMapping { 158 if path.matches(pattern) { 159 return caster, true 160 } 161 } 162 return nil, false 163 }