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