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  }