github.com/panekj/cli@v0.0.0-20230304125325-467dd2f3797e/cli/compose/template/template.go (about)

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