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  }