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