github.com/ali-iotechsys/cli@v20.10.0+incompatible/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 case string: 58 newValue, err := opts.Substitute(value, template.Mapping(opts.LookupValue)) 59 if err != nil || newValue == value { 60 return value, newPathError(path, err) 61 } 62 caster, ok := opts.getCasterForPath(path) 63 if !ok { 64 return newValue, nil 65 } 66 casted, err := caster(newValue) 67 return casted, newPathError(path, errors.Wrap(err, "failed to cast to expected type")) 68 69 case map[string]interface{}: 70 out := map[string]interface{}{} 71 for key, elem := range value { 72 interpolatedElem, err := recursiveInterpolate(elem, path.Next(key), opts) 73 if err != nil { 74 return nil, err 75 } 76 out[key] = interpolatedElem 77 } 78 return out, nil 79 80 case []interface{}: 81 out := make([]interface{}, len(value)) 82 for i, elem := range value { 83 interpolatedElem, err := recursiveInterpolate(elem, path.Next(PathMatchList), opts) 84 if err != nil { 85 return nil, err 86 } 87 out[i] = interpolatedElem 88 } 89 return out, nil 90 91 default: 92 return value, nil 93 } 94 } 95 96 func newPathError(path Path, err error) error { 97 switch err := err.(type) { 98 case nil: 99 return nil 100 case *template.InvalidTemplateError: 101 return errors.Errorf( 102 "invalid interpolation format for %s: %#v. You may need to escape any $ with another $.", 103 path, err.Template) 104 default: 105 return errors.Wrapf(err, "error while interpolating %s", path) 106 } 107 } 108 109 const pathSeparator = "." 110 111 // PathMatchAll is a token used as part of a Path to match any key at that level 112 // in the nested structure 113 const PathMatchAll = "*" 114 115 // PathMatchList is a token used as part of a Path to match items in a list 116 const PathMatchList = "[]" 117 118 // Path is a dotted path of keys to a value in a nested mapping structure. A * 119 // section in a path will match any key in the mapping structure. 120 type Path string 121 122 // NewPath returns a new Path 123 func NewPath(items ...string) Path { 124 return Path(strings.Join(items, pathSeparator)) 125 } 126 127 // Next returns a new path by append part to the current path 128 func (p Path) Next(part string) Path { 129 return Path(string(p) + pathSeparator + part) 130 } 131 132 func (p Path) parts() []string { 133 return strings.Split(string(p), pathSeparator) 134 } 135 136 func (p Path) matches(pattern Path) bool { 137 patternParts := pattern.parts() 138 parts := p.parts() 139 140 if len(patternParts) != len(parts) { 141 return false 142 } 143 for index, part := range parts { 144 switch patternParts[index] { 145 case PathMatchAll, part: 146 continue 147 default: 148 return false 149 } 150 } 151 return true 152 } 153 154 func (o Options) getCasterForPath(path Path) (Cast, bool) { 155 for pattern, caster := range o.TypeCastMapping { 156 if path.matches(pattern) { 157 return caster, true 158 } 159 } 160 return nil, false 161 }