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  }