k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/generators/markers.go (about)

     1  /*
     2  Copyright 2022 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8  	http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package generators
    18  
    19  import (
    20  	"encoding/json"
    21  	"errors"
    22  	"fmt"
    23  	"regexp"
    24  	"strconv"
    25  	"strings"
    26  
    27  	"k8s.io/gengo/v2/types"
    28  	openapi "k8s.io/kube-openapi/pkg/common"
    29  	"k8s.io/kube-openapi/pkg/validation/spec"
    30  )
    31  
    32  type CELTag struct {
    33  	Rule              string `json:"rule,omitempty"`
    34  	Message           string `json:"message,omitempty"`
    35  	MessageExpression string `json:"messageExpression,omitempty"`
    36  	OptionalOldSelf   *bool  `json:"optionalOldSelf,omitempty"`
    37  	Reason            string `json:"reason,omitempty"`
    38  	FieldPath         string `json:"fieldPath,omitempty"`
    39  }
    40  
    41  func (c *CELTag) Validate() error {
    42  	if c == nil || *c == (CELTag{}) {
    43  		return fmt.Errorf("empty CEL tag is not allowed")
    44  	}
    45  
    46  	var errs []error
    47  	if c.Rule == "" {
    48  		errs = append(errs, fmt.Errorf("rule cannot be empty"))
    49  	}
    50  	if c.Message == "" && c.MessageExpression == "" {
    51  		errs = append(errs, fmt.Errorf("message or messageExpression must be set"))
    52  	}
    53  	if c.Message != "" && c.MessageExpression != "" {
    54  		errs = append(errs, fmt.Errorf("message and messageExpression cannot be set at the same time"))
    55  	}
    56  
    57  	if len(errs) > 0 {
    58  		return errors.Join(errs...)
    59  	}
    60  
    61  	return nil
    62  }
    63  
    64  // commentTags represents the parsed comment tags for a given type. These types are then used to generate schema validations.
    65  // These only include the newer prefixed tags. The older tags are still supported,
    66  // but are not included in this struct. Comment Tags are transformed into a
    67  // *spec.Schema, which is then combined with the older marker comments to produce
    68  // the generated OpenAPI spec.
    69  //
    70  // List of tags not included in this struct:
    71  //
    72  // - +optional
    73  // - +default
    74  // - +listType
    75  // - +listMapKeys
    76  // - +mapType
    77  type commentTags struct {
    78  	spec.SchemaProps
    79  
    80  	CEL []CELTag `json:"cel,omitempty"`
    81  
    82  	// Future markers can all be parsed into this centralized struct...
    83  	// Optional bool `json:"optional,omitempty"`
    84  	// Default  any  `json:"default,omitempty"`
    85  }
    86  
    87  // Returns the schema for the given CommentTags instance.
    88  // This is the final authoritative schema for the comment tags
    89  func (c commentTags) ValidationSchema() (*spec.Schema, error) {
    90  	res := spec.Schema{
    91  		SchemaProps: c.SchemaProps,
    92  	}
    93  
    94  	if len(c.CEL) > 0 {
    95  		// Convert the CELTag to a map[string]interface{} via JSON
    96  		celTagJSON, err := json.Marshal(c.CEL)
    97  		if err != nil {
    98  			return nil, fmt.Errorf("failed to marshal CEL tag: %w", err)
    99  		}
   100  		var celTagMap []interface{}
   101  		if err := json.Unmarshal(celTagJSON, &celTagMap); err != nil {
   102  			return nil, fmt.Errorf("failed to unmarshal CEL tag: %w", err)
   103  		}
   104  
   105  		res.VendorExtensible.AddExtension("x-kubernetes-validations", celTagMap)
   106  	}
   107  
   108  	return &res, nil
   109  }
   110  
   111  // validates the parameters in a CommentTags instance. Returns any errors encountered.
   112  func (c commentTags) Validate() error {
   113  
   114  	var err error
   115  
   116  	if c.MinLength != nil && *c.MinLength < 0 {
   117  		err = errors.Join(err, fmt.Errorf("minLength cannot be negative"))
   118  	}
   119  	if c.MaxLength != nil && *c.MaxLength < 0 {
   120  		err = errors.Join(err, fmt.Errorf("maxLength cannot be negative"))
   121  	}
   122  	if c.MinItems != nil && *c.MinItems < 0 {
   123  		err = errors.Join(err, fmt.Errorf("minItems cannot be negative"))
   124  	}
   125  	if c.MaxItems != nil && *c.MaxItems < 0 {
   126  		err = errors.Join(err, fmt.Errorf("maxItems cannot be negative"))
   127  	}
   128  	if c.MinProperties != nil && *c.MinProperties < 0 {
   129  		err = errors.Join(err, fmt.Errorf("minProperties cannot be negative"))
   130  	}
   131  	if c.MaxProperties != nil && *c.MaxProperties < 0 {
   132  		err = errors.Join(err, fmt.Errorf("maxProperties cannot be negative"))
   133  	}
   134  	if c.Minimum != nil && c.Maximum != nil && *c.Minimum > *c.Maximum {
   135  		err = errors.Join(err, fmt.Errorf("minimum %f is greater than maximum %f", *c.Minimum, *c.Maximum))
   136  	}
   137  	if (c.ExclusiveMinimum || c.ExclusiveMaximum) && c.Minimum != nil && c.Maximum != nil && *c.Minimum == *c.Maximum {
   138  		err = errors.Join(err, fmt.Errorf("exclusiveMinimum/Maximum cannot be set when minimum == maximum"))
   139  	}
   140  	if c.MinLength != nil && c.MaxLength != nil && *c.MinLength > *c.MaxLength {
   141  		err = errors.Join(err, fmt.Errorf("minLength %d is greater than maxLength %d", *c.MinLength, *c.MaxLength))
   142  	}
   143  	if c.MinItems != nil && c.MaxItems != nil && *c.MinItems > *c.MaxItems {
   144  		err = errors.Join(err, fmt.Errorf("minItems %d is greater than maxItems %d", *c.MinItems, *c.MaxItems))
   145  	}
   146  	if c.MinProperties != nil && c.MaxProperties != nil && *c.MinProperties > *c.MaxProperties {
   147  		err = errors.Join(err, fmt.Errorf("minProperties %d is greater than maxProperties %d", *c.MinProperties, *c.MaxProperties))
   148  	}
   149  	if c.Pattern != "" {
   150  		_, e := regexp.Compile(c.Pattern)
   151  		if e != nil {
   152  			err = errors.Join(err, fmt.Errorf("invalid pattern %q: %v", c.Pattern, e))
   153  		}
   154  	}
   155  	if c.MultipleOf != nil && *c.MultipleOf == 0 {
   156  		err = errors.Join(err, fmt.Errorf("multipleOf cannot be 0"))
   157  	}
   158  
   159  	for i, celTag := range c.CEL {
   160  		celError := celTag.Validate()
   161  		if celError == nil {
   162  			continue
   163  		}
   164  		err = errors.Join(err, fmt.Errorf("invalid CEL tag at index %d: %w", i, celError))
   165  	}
   166  
   167  	return err
   168  }
   169  
   170  // Performs type-specific validation for CommentTags porameters. Accepts a Type instance and returns any errors encountered during validation.
   171  func (c commentTags) ValidateType(t *types.Type) error {
   172  	var err error
   173  
   174  	resolvedType := resolveAliasAndPtrType(t)
   175  	typeString, _ := openapi.OpenAPITypeFormat(resolvedType.String()) // will be empty for complicated types
   176  
   177  	// Structs and interfaces may dynamically be any type, so we cant validate them
   178  	// easily. We may be able to if we check that they don't implement all the
   179  	// override functions, but for now we just skip them.
   180  	if resolvedType.Kind == types.Interface || resolvedType.Kind == types.Struct {
   181  		return nil
   182  	}
   183  
   184  	isArray := resolvedType.Kind == types.Slice || resolvedType.Kind == types.Array
   185  	isMap := resolvedType.Kind == types.Map
   186  	isString := typeString == "string"
   187  	isInt := typeString == "integer"
   188  	isFloat := typeString == "number"
   189  
   190  	if c.MaxItems != nil && !isArray {
   191  		err = errors.Join(err, fmt.Errorf("maxItems can only be used on array types"))
   192  	}
   193  	if c.MinItems != nil && !isArray {
   194  		err = errors.Join(err, fmt.Errorf("minItems can only be used on array types"))
   195  	}
   196  	if c.UniqueItems && !isArray {
   197  		err = errors.Join(err, fmt.Errorf("uniqueItems can only be used on array types"))
   198  	}
   199  	if c.MaxProperties != nil && !isMap {
   200  		err = errors.Join(err, fmt.Errorf("maxProperties can only be used on map types"))
   201  	}
   202  	if c.MinProperties != nil && !isMap {
   203  		err = errors.Join(err, fmt.Errorf("minProperties can only be used on map types"))
   204  	}
   205  	if c.MinLength != nil && !isString {
   206  		err = errors.Join(err, fmt.Errorf("minLength can only be used on string types"))
   207  	}
   208  	if c.MaxLength != nil && !isString {
   209  		err = errors.Join(err, fmt.Errorf("maxLength can only be used on string types"))
   210  	}
   211  	if c.Pattern != "" && !isString {
   212  		err = errors.Join(err, fmt.Errorf("pattern can only be used on string types"))
   213  	}
   214  	if c.Minimum != nil && !isInt && !isFloat {
   215  		err = errors.Join(err, fmt.Errorf("minimum can only be used on numeric types"))
   216  	}
   217  	if c.Maximum != nil && !isInt && !isFloat {
   218  		err = errors.Join(err, fmt.Errorf("maximum can only be used on numeric types"))
   219  	}
   220  	if c.MultipleOf != nil && !isInt && !isFloat {
   221  		err = errors.Join(err, fmt.Errorf("multipleOf can only be used on numeric types"))
   222  	}
   223  	if c.ExclusiveMinimum && !isInt && !isFloat {
   224  		err = errors.Join(err, fmt.Errorf("exclusiveMinimum can only be used on numeric types"))
   225  	}
   226  	if c.ExclusiveMaximum && !isInt && !isFloat {
   227  		err = errors.Join(err, fmt.Errorf("exclusiveMaximum can only be used on numeric types"))
   228  	}
   229  
   230  	return err
   231  }
   232  
   233  // Parses the given comments into a CommentTags type. Validates the parsed comment tags, and returns the result.
   234  // Accepts an optional type to validate against, and a prefix to filter out markers not related to validation.
   235  // Accepts a prefix to filter out markers not related to validation.
   236  // Returns any errors encountered while parsing or validating the comment tags.
   237  func ParseCommentTags(t *types.Type, comments []string, prefix string) (*spec.Schema, error) {
   238  
   239  	markers, err := parseMarkers(comments, prefix)
   240  	if err != nil {
   241  		return nil, fmt.Errorf("failed to parse marker comments: %w", err)
   242  	}
   243  	nested, err := nestMarkers(markers)
   244  	if err != nil {
   245  		return nil, fmt.Errorf("invalid marker comments: %w", err)
   246  	}
   247  
   248  	// Parse the map into a CommentTags type by marshalling and unmarshalling
   249  	// as JSON in leiu of an unstructured converter.
   250  	out, err := json.Marshal(nested)
   251  	if err != nil {
   252  		return nil, fmt.Errorf("failed to marshal marker comments: %w", err)
   253  	}
   254  
   255  	var commentTags commentTags
   256  	if err = json.Unmarshal(out, &commentTags); err != nil {
   257  		return nil, fmt.Errorf("failed to unmarshal marker comments: %w", err)
   258  	}
   259  
   260  	// Validate the parsed comment tags
   261  	validationErrors := commentTags.Validate()
   262  
   263  	if t != nil {
   264  		validationErrors = errors.Join(validationErrors, commentTags.ValidateType(t))
   265  	}
   266  
   267  	if validationErrors != nil {
   268  		return nil, fmt.Errorf("invalid marker comments: %w", validationErrors)
   269  	}
   270  
   271  	return commentTags.ValidationSchema()
   272  }
   273  
   274  var (
   275  	allowedKeyCharacterSet = `[:_a-zA-Z0-9\[\]\-]`
   276  	valueEmpty             = regexp.MustCompile(fmt.Sprintf(`^(%s*)$`, allowedKeyCharacterSet))
   277  	valueAssign            = regexp.MustCompile(fmt.Sprintf(`^(%s*)=(.*)$`, allowedKeyCharacterSet))
   278  	valueRawString         = regexp.MustCompile(fmt.Sprintf(`^(%s*)>(.*)$`, allowedKeyCharacterSet))
   279  )
   280  
   281  // extractCommentTags parses comments for lines of the form:
   282  //
   283  //	'marker' + "key=value"
   284  //
   285  //	or to specify truthy boolean keys:
   286  //
   287  //	'marker' + "key"
   288  //
   289  // Values are optional; "" is the default.  A tag can be specified more than
   290  // one time and all values are returned.  Returns a map with an entry for
   291  // for each key and a value.
   292  //
   293  // Similar to version from gengo, but this version support only allows one
   294  // value per key (preferring explicit array indices), supports raw strings
   295  // with concatenation, and limits the usable characters allowed in a key
   296  // (for simpler parsing).
   297  //
   298  // Assignments and empty values have the same syntax as from gengo. Raw strings
   299  // have the syntax:
   300  //
   301  //	'marker' + "key>value"
   302  //	'marker' + "key>value"
   303  //
   304  // Successive usages of the same raw string key results in concatenating each
   305  // line with `\n` in between. It is an error to use `=` to assing to a previously
   306  // assigned key
   307  // (in contrast to types.ExtractCommentTags which allows array-typed
   308  // values to be specified using `=`).
   309  func extractCommentTags(marker string, lines []string) (map[string]string, error) {
   310  	out := map[string]string{}
   311  
   312  	// Used to track the the line immediately prior to the one being iterated.
   313  	// If there was an invalid or ignored line, these values get reset.
   314  	lastKey := ""
   315  	lastIndex := -1
   316  	lastArrayKey := ""
   317  
   318  	var lintErrors []error
   319  
   320  	for _, line := range lines {
   321  		line = strings.Trim(line, " ")
   322  
   323  		// Track the current value of the last vars to use in this loop iteration
   324  		// before they are reset for the next iteration.
   325  		previousKey := lastKey
   326  		previousArrayKey := lastArrayKey
   327  		previousIndex := lastIndex
   328  
   329  		// Make sure last vars gets reset if we `continue`
   330  		lastIndex = -1
   331  		lastArrayKey = ""
   332  		lastKey = ""
   333  
   334  		if len(line) == 0 {
   335  			continue
   336  		} else if !strings.HasPrefix(line, marker) {
   337  			continue
   338  		}
   339  
   340  		line = strings.TrimPrefix(line, marker)
   341  
   342  		key := ""
   343  		value := ""
   344  
   345  		if matches := valueAssign.FindStringSubmatch(line); matches != nil {
   346  			key = matches[1]
   347  			value = matches[2]
   348  
   349  			// If key exists, throw error.
   350  			// Some of the old kube open-api gen marker comments like
   351  			// `+listMapKeys` allowed a list to be specified by writing key=value
   352  			// multiple times.
   353  			//
   354  			// This is not longer supported for the prefixed marker comments.
   355  			// This is to prevent confusion with the new array syntax which
   356  			// supports lists of objects.
   357  			//
   358  			// The old marker comments like +listMapKeys will remain functional,
   359  			// but new markers will not support it.
   360  			if _, ok := out[key]; ok {
   361  				return nil, fmt.Errorf("cannot have multiple values for key '%v'", key)
   362  			}
   363  
   364  		} else if matches := valueEmpty.FindStringSubmatch(line); matches != nil {
   365  			key = matches[1]
   366  			value = ""
   367  
   368  		} else if matches := valueRawString.FindStringSubmatch(line); matches != nil {
   369  			toAdd := strings.Trim(string(matches[2]), " ")
   370  
   371  			key = matches[1]
   372  
   373  			// First usage as a raw string.
   374  			if existing, exists := out[key]; !exists {
   375  
   376  				// Encode the raw string as JSON to ensure that it is properly escaped.
   377  				valueBytes, err := json.Marshal(toAdd)
   378  				if err != nil {
   379  					return nil, fmt.Errorf("invalid value for key %v: %w", key, err)
   380  				}
   381  
   382  				value = string(valueBytes)
   383  			} else if key != previousKey {
   384  				// Successive usages of the same key of a raw string must be
   385  				// consecutive
   386  				return nil, fmt.Errorf("concatenations to key '%s' must be consecutive with its assignment", key)
   387  			} else {
   388  				// If it is a consecutive repeat usage, concatenate to the
   389  				// existing value.
   390  				//
   391  				// Decode JSON string, append to it, re-encode JSON string.
   392  				// Kinda janky but this is a code-generator...
   393  				var unmarshalled string
   394  				if err := json.Unmarshal([]byte(existing), &unmarshalled); err != nil {
   395  					return nil, fmt.Errorf("invalid value for key %v: %w", key, err)
   396  				} else {
   397  					unmarshalled += "\n" + toAdd
   398  					valueBytes, err := json.Marshal(unmarshalled)
   399  					if err != nil {
   400  						return nil, fmt.Errorf("invalid value for key %v: %w", key, err)
   401  					}
   402  
   403  					value = string(valueBytes)
   404  				}
   405  			}
   406  		} else {
   407  			// Comment has the correct prefix, but incorrect syntax, so it is an
   408  			// error
   409  			return nil, fmt.Errorf("invalid marker comment does not match expected `+key=<json formatted value>` pattern: %v", line)
   410  		}
   411  
   412  		out[key] = value
   413  		lastKey = key
   414  
   415  		// Lint the array subscript for common mistakes. This only lints the last
   416  		// array index used, (since we do not have a need for nested arrays yet
   417  		// in markers)
   418  		if arrayPath, index, hasSubscript, err := extractArraySubscript(key); hasSubscript {
   419  			// If index is non-zero, check that that previous line was for the same
   420  			// key and either the same or previous index
   421  			if err != nil {
   422  				lintErrors = append(lintErrors, fmt.Errorf("error parsing %v: expected integer index in key '%v'", line, key))
   423  			} else if previousArrayKey != arrayPath && index != 0 {
   424  				lintErrors = append(lintErrors, fmt.Errorf("error parsing %v: non-consecutive index %v for key '%v'", line, index, arrayPath))
   425  			} else if index != previousIndex+1 && index != previousIndex {
   426  				lintErrors = append(lintErrors, fmt.Errorf("error parsing %v: non-consecutive index %v for key '%v'", line, index, arrayPath))
   427  			}
   428  
   429  			lastIndex = index
   430  			lastArrayKey = arrayPath
   431  		}
   432  	}
   433  
   434  	if len(lintErrors) > 0 {
   435  		return nil, errors.Join(lintErrors...)
   436  	}
   437  
   438  	return out, nil
   439  }
   440  
   441  // Extracts and parses the given marker comments into a map of key -> value.
   442  // Accepts a prefix to filter out markers not related to validation.
   443  // The prefix is removed from the key in the returned map.
   444  // Empty keys and invalid values will return errors, refs are currently unsupported and will be skipped.
   445  func parseMarkers(markerComments []string, prefix string) (map[string]any, error) {
   446  	markers, err := extractCommentTags(prefix, markerComments)
   447  	if err != nil {
   448  		return nil, err
   449  	}
   450  
   451  	// Parse the values as JSON
   452  	result := map[string]any{}
   453  	for key, value := range markers {
   454  		var unmarshalled interface{}
   455  
   456  		if len(key) == 0 {
   457  			return nil, fmt.Errorf("cannot have empty key for marker comment")
   458  		} else if _, ok := parseSymbolReference(value, ""); ok {
   459  			// Skip ref markers
   460  			continue
   461  		} else if len(value) == 0 {
   462  			// Empty value means key is implicitly a bool
   463  			result[key] = true
   464  		} else if err := json.Unmarshal([]byte(value), &unmarshalled); err != nil {
   465  			// Not valid JSON, throw error
   466  			return nil, fmt.Errorf("failed to parse value for key %v as JSON: %w", key, err)
   467  		} else {
   468  			// Is is valid JSON, use as a JSON value
   469  			result[key] = unmarshalled
   470  		}
   471  	}
   472  	return result, nil
   473  }
   474  
   475  // Converts a map of:
   476  //
   477  //	"a:b:c": 1
   478  //	"a:b:d": 2
   479  //	"a:e": 3
   480  //	"f": 4
   481  //
   482  // Into:
   483  //
   484  //	 map[string]any{
   485  //	   "a": map[string]any{
   486  //		      "b": map[string]any{
   487  //		          "c": 1,
   488  //				  "d": 2,
   489  //			   },
   490  //			   "e": 3,
   491  //		  },
   492  //		  "f": 4,
   493  //	 }
   494  //
   495  // Returns a list of joined errors for any invalid keys. See putNestedValue for more details.
   496  func nestMarkers(markers map[string]any) (map[string]any, error) {
   497  	nested := make(map[string]any)
   498  	var errs []error
   499  	for key, value := range markers {
   500  		var err error
   501  		keys := strings.Split(key, ":")
   502  
   503  		if err = putNestedValue(nested, keys, value); err != nil {
   504  			errs = append(errs, err)
   505  		}
   506  	}
   507  
   508  	if len(errs) > 0 {
   509  		return nil, errors.Join(errs...)
   510  	}
   511  
   512  	return nested, nil
   513  }
   514  
   515  // Recursively puts a value into the given keypath, creating intermediate maps
   516  // and slices as needed. If a key is of the form `foo[bar]`, then bar will be
   517  // treated as an index into the array foo. If bar is not a valid integer, putNestedValue returns an error.
   518  func putNestedValue(m map[string]any, k []string, v any) error {
   519  	if len(k) == 0 {
   520  		return nil
   521  	}
   522  
   523  	key := k[0]
   524  	rest := k[1:]
   525  
   526  	// Array case
   527  	if arrayKeyWithoutSubscript, index, hasSubscript, err := extractArraySubscript(key); err != nil {
   528  		return fmt.Errorf("error parsing subscript for key %v: %w", key, err)
   529  	} else if hasSubscript {
   530  		key = arrayKeyWithoutSubscript
   531  		var arrayDestination []any
   532  		if existing, ok := m[key]; !ok {
   533  			arrayDestination = make([]any, index+1)
   534  		} else if existing, ok := existing.([]any); !ok {
   535  			// Error case. Existing isn't of correct type. Can happen if
   536  			// someone is subscripting a field that was previously not an array
   537  			return fmt.Errorf("expected []any at key %v, got %T", key, existing)
   538  		} else if index >= len(existing) {
   539  			// Ensure array is big enough
   540  			arrayDestination = append(existing, make([]any, index-len(existing)+1)...)
   541  		} else {
   542  			arrayDestination = existing
   543  		}
   544  
   545  		m[key] = arrayDestination
   546  		if arrayDestination[index] == nil {
   547  			// Doesn't exist case, create the destination.
   548  			// Assumes the destination is a map for now. Theoretically could be
   549  			// extended to support arrays of arrays, but that's not needed yet.
   550  			destination := make(map[string]any)
   551  			arrayDestination[index] = destination
   552  			if err = putNestedValue(destination, rest, v); err != nil {
   553  				return err
   554  			}
   555  		} else if dst, ok := arrayDestination[index].(map[string]any); ok {
   556  			// Already exists case, correct type
   557  			if putNestedValue(dst, rest, v); err != nil {
   558  				return err
   559  			}
   560  		} else {
   561  			// Already exists, incorrect type. Error
   562  			// This shouldn't be possible.
   563  			return fmt.Errorf("expected map at %v[%v], got %T", key, index, arrayDestination[index])
   564  		}
   565  
   566  		return nil
   567  	} else if len(rest) == 0 {
   568  		// Base case. Single key. Just set into destination
   569  		m[key] = v
   570  		return nil
   571  	}
   572  
   573  	if existing, ok := m[key]; !ok {
   574  		destination := make(map[string]any)
   575  		m[key] = destination
   576  		return putNestedValue(destination, rest, v)
   577  	} else if destination, ok := existing.(map[string]any); ok {
   578  		return putNestedValue(destination, rest, v)
   579  	} else {
   580  		// Error case. Existing isn't of correct type. Can happen if prior comment
   581  		// referred to value as an error
   582  		return fmt.Errorf("expected map[string]any at key %v, got %T", key, existing)
   583  	}
   584  }
   585  
   586  // extractArraySubscript extracts the left array subscript from a key of
   587  // the form  `foo[bar][baz]` -> "bar".
   588  // Returns the key without the subscript, the index, and a bool indicating if
   589  // the key had a subscript.
   590  // If the key has a subscript, but the subscript is not a valid integer, returns an error.
   591  //
   592  // This can be adapted to support multidimensional subscripts probably fairly
   593  // easily by retuning a list of ints
   594  func extractArraySubscript(str string) (string, int, bool, error) {
   595  	subscriptIdx := strings.Index(str, "[")
   596  	if subscriptIdx == -1 {
   597  		return "", -1, false, nil
   598  	}
   599  
   600  	subscript := strings.Split(str[subscriptIdx+1:], "]")[0]
   601  	if len(subscript) == 0 {
   602  		return "", -1, false, fmt.Errorf("empty subscript not allowed")
   603  	}
   604  
   605  	index, err := strconv.Atoi(subscript)
   606  	if err != nil {
   607  		return "", -1, false, fmt.Errorf("expected integer index in key %v", str)
   608  	} else if index < 0 {
   609  		return "", -1, false, fmt.Errorf("subscript '%v' is invalid. index must be positive", subscript)
   610  	}
   611  
   612  	return str[:subscriptIdx], index, true, nil
   613  }