github.com/ali-iotechsys/cli@v20.10.0+incompatible/cli/compose/template/template.go (about)

     1  package template
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"strings"
     7  )
     8  
     9  var delimiter = "\\$"
    10  var substitution = "[_a-z][_a-z0-9]*(?::?[-?][^}]*)?"
    11  
    12  var patternString = fmt.Sprintf(
    13  	"%s(?i:(?P<escaped>%s)|(?P<named>%s)|{(?P<braced>%s)}|(?P<invalid>))",
    14  	delimiter, delimiter, substitution, substitution,
    15  )
    16  
    17  var defaultPattern = regexp.MustCompile(patternString)
    18  
    19  // DefaultSubstituteFuncs contains the default SubstituteFunc used by the docker cli
    20  var DefaultSubstituteFuncs = []SubstituteFunc{
    21  	softDefault,
    22  	hardDefault,
    23  	requiredNonEmpty,
    24  	required,
    25  }
    26  
    27  // InvalidTemplateError is returned when a variable template is not in a valid
    28  // format
    29  type InvalidTemplateError struct {
    30  	Template string
    31  }
    32  
    33  func (e InvalidTemplateError) Error() string {
    34  	return fmt.Sprintf("Invalid template: %#v", e.Template)
    35  }
    36  
    37  // Mapping is a user-supplied function which maps from variable names to values.
    38  // Returns the value as a string and a bool indicating whether
    39  // the value is present, to distinguish between an empty string
    40  // and the absence of a value.
    41  type Mapping func(string) (string, bool)
    42  
    43  // SubstituteFunc is a user-supplied function that apply substitution.
    44  // Returns the value as a string, a bool indicating if the function could apply
    45  // the substitution and an error.
    46  type SubstituteFunc func(string, Mapping) (string, bool, error)
    47  
    48  // SubstituteWith subsitute variables in the string with their values.
    49  // It accepts additional substitute function.
    50  func SubstituteWith(template string, mapping Mapping, pattern *regexp.Regexp, subsFuncs ...SubstituteFunc) (string, error) {
    51  	var err error
    52  	result := pattern.ReplaceAllStringFunc(template, func(substring string) string {
    53  		matches := pattern.FindStringSubmatch(substring)
    54  		groups := matchGroups(matches, pattern)
    55  		if escaped := groups["escaped"]; escaped != "" {
    56  			return escaped
    57  		}
    58  
    59  		substitution := groups["named"]
    60  		if substitution == "" {
    61  			substitution = groups["braced"]
    62  		}
    63  
    64  		if substitution == "" {
    65  			err = &InvalidTemplateError{Template: template}
    66  			return ""
    67  		}
    68  
    69  		for _, f := range subsFuncs {
    70  			var (
    71  				value   string
    72  				applied bool
    73  			)
    74  			value, applied, err = f(substitution, mapping)
    75  			if err != nil {
    76  				return ""
    77  			}
    78  			if !applied {
    79  				continue
    80  			}
    81  			return value
    82  		}
    83  
    84  		value, _ := mapping(substitution)
    85  		return value
    86  	})
    87  
    88  	return result, err
    89  }
    90  
    91  // Substitute variables in the string with their values
    92  func Substitute(template string, mapping Mapping) (string, error) {
    93  	return SubstituteWith(template, mapping, defaultPattern, DefaultSubstituteFuncs...)
    94  }
    95  
    96  // ExtractVariables returns a map of all the variables defined in the specified
    97  // composefile (dict representation) and their default value if any.
    98  func ExtractVariables(configDict map[string]interface{}, pattern *regexp.Regexp) map[string]string {
    99  	if pattern == nil {
   100  		pattern = defaultPattern
   101  	}
   102  	return recurseExtract(configDict, pattern)
   103  }
   104  
   105  func recurseExtract(value interface{}, pattern *regexp.Regexp) map[string]string {
   106  	m := map[string]string{}
   107  
   108  	switch value := value.(type) {
   109  	case string:
   110  		if values, is := extractVariable(value, pattern); is {
   111  			for _, v := range values {
   112  				m[v.name] = v.value
   113  			}
   114  		}
   115  	case map[string]interface{}:
   116  		for _, elem := range value {
   117  			submap := recurseExtract(elem, pattern)
   118  			for key, value := range submap {
   119  				m[key] = value
   120  			}
   121  		}
   122  
   123  	case []interface{}:
   124  		for _, elem := range value {
   125  			if values, is := extractVariable(elem, pattern); is {
   126  				for _, v := range values {
   127  					m[v.name] = v.value
   128  				}
   129  			}
   130  		}
   131  	}
   132  
   133  	return m
   134  }
   135  
   136  type extractedValue struct {
   137  	name  string
   138  	value string
   139  }
   140  
   141  func extractVariable(value interface{}, pattern *regexp.Regexp) ([]extractedValue, bool) {
   142  	sValue, ok := value.(string)
   143  	if !ok {
   144  		return []extractedValue{}, false
   145  	}
   146  	matches := pattern.FindAllStringSubmatch(sValue, -1)
   147  	if len(matches) == 0 {
   148  		return []extractedValue{}, false
   149  	}
   150  	values := []extractedValue{}
   151  	for _, match := range matches {
   152  		groups := matchGroups(match, pattern)
   153  		if escaped := groups["escaped"]; escaped != "" {
   154  			continue
   155  		}
   156  		val := groups["named"]
   157  		if val == "" {
   158  			val = groups["braced"]
   159  		}
   160  		name := val
   161  		var defaultValue string
   162  		switch {
   163  		case strings.Contains(val, ":?"):
   164  			name, _ = partition(val, ":?")
   165  		case strings.Contains(val, "?"):
   166  			name, _ = partition(val, "?")
   167  		case strings.Contains(val, ":-"):
   168  			name, defaultValue = partition(val, ":-")
   169  		case strings.Contains(val, "-"):
   170  			name, defaultValue = partition(val, "-")
   171  		}
   172  		values = append(values, extractedValue{name: name, value: defaultValue})
   173  	}
   174  	return values, len(values) > 0
   175  }
   176  
   177  // Soft default (fall back if unset or empty)
   178  func softDefault(substitution string, mapping Mapping) (string, bool, error) {
   179  	sep := ":-"
   180  	if !strings.Contains(substitution, sep) {
   181  		return "", false, nil
   182  	}
   183  	name, defaultValue := partition(substitution, sep)
   184  	value, ok := mapping(name)
   185  	if !ok || value == "" {
   186  		return defaultValue, true, nil
   187  	}
   188  	return value, true, nil
   189  }
   190  
   191  // Hard default (fall back if-and-only-if empty)
   192  func hardDefault(substitution string, mapping Mapping) (string, bool, error) {
   193  	sep := "-"
   194  	if !strings.Contains(substitution, sep) {
   195  		return "", false, nil
   196  	}
   197  	name, defaultValue := partition(substitution, sep)
   198  	value, ok := mapping(name)
   199  	if !ok {
   200  		return defaultValue, true, nil
   201  	}
   202  	return value, true, nil
   203  }
   204  
   205  func requiredNonEmpty(substitution string, mapping Mapping) (string, bool, error) {
   206  	return withRequired(substitution, mapping, ":?", func(v string) bool { return v != "" })
   207  }
   208  
   209  func required(substitution string, mapping Mapping) (string, bool, error) {
   210  	return withRequired(substitution, mapping, "?", func(_ string) bool { return true })
   211  }
   212  
   213  func withRequired(substitution string, mapping Mapping, sep string, valid func(string) bool) (string, bool, error) {
   214  	if !strings.Contains(substitution, sep) {
   215  		return "", false, nil
   216  	}
   217  	name, errorMessage := partition(substitution, sep)
   218  	value, ok := mapping(name)
   219  	if !ok || !valid(value) {
   220  		return "", true, &InvalidTemplateError{
   221  			Template: fmt.Sprintf("required variable %s is missing a value: %s", name, errorMessage),
   222  		}
   223  	}
   224  	return value, true, nil
   225  }
   226  
   227  func matchGroups(matches []string, pattern *regexp.Regexp) map[string]string {
   228  	groups := make(map[string]string)
   229  	for i, name := range pattern.SubexpNames()[1:] {
   230  		groups[name] = matches[i+1]
   231  	}
   232  	return groups
   233  }
   234  
   235  // Split the string at the first occurrence of sep, and return the part before the separator,
   236  // and the part after the separator.
   237  //
   238  // If the separator is not found, return the string itself, followed by an empty string.
   239  func partition(s, sep string) (string, string) {
   240  	if strings.Contains(s, sep) {
   241  		parts := strings.SplitN(s, sep, 2)
   242  		return parts[0], parts[1]
   243  	}
   244  	return s, ""
   245  }