k8s.io/kube-openapi@v0.0.0-20240826222958-65a50c78dec5/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  	Nullable         *bool         `json:"nullable,omitempty"`
    79  	Format           *string       `json:"format,omitempty"`
    80  	Maximum          *float64      `json:"maximum,omitempty"`
    81  	ExclusiveMaximum *bool         `json:"exclusiveMaximum,omitempty"`
    82  	Minimum          *float64      `json:"minimum,omitempty"`
    83  	ExclusiveMinimum *bool         `json:"exclusiveMinimum,omitempty"`
    84  	MaxLength        *int64        `json:"maxLength,omitempty"`
    85  	MinLength        *int64        `json:"minLength,omitempty"`
    86  	Pattern          *string       `json:"pattern,omitempty"`
    87  	MaxItems         *int64        `json:"maxItems,omitempty"`
    88  	MinItems         *int64        `json:"minItems,omitempty"`
    89  	UniqueItems      *bool         `json:"uniqueItems,omitempty"`
    90  	MultipleOf       *float64      `json:"multipleOf,omitempty"`
    91  	Enum             []interface{} `json:"enum,omitempty"`
    92  	MaxProperties    *int64        `json:"maxProperties,omitempty"`
    93  	MinProperties    *int64        `json:"minProperties,omitempty"`
    94  
    95  	// Nested commentTags for extending the schemas of subfields at point-of-use
    96  	// when you cant annotate them directly. Cannot be used to add properties
    97  	// or remove validations on the overridden schema.
    98  	Items                *commentTags            `json:"items,omitempty"`
    99  	Properties           map[string]*commentTags `json:"properties,omitempty"`
   100  	AdditionalProperties *commentTags            `json:"additionalProperties,omitempty"`
   101  
   102  	CEL []CELTag `json:"cel,omitempty"`
   103  
   104  	// Future markers can all be parsed into this centralized struct...
   105  	// Optional bool `json:"optional,omitempty"`
   106  	// Default  any  `json:"default,omitempty"`
   107  }
   108  
   109  // Returns the schema for the given CommentTags instance.
   110  // This is the final authoritative schema for the comment tags
   111  func (c *commentTags) ValidationSchema() (*spec.Schema, error) {
   112  	if c == nil {
   113  		return nil, nil
   114  	}
   115  
   116  	isNullable := c.Nullable != nil && *c.Nullable
   117  	format := ""
   118  	if c.Format != nil {
   119  		format = *c.Format
   120  	}
   121  	isExclusiveMaximum := c.ExclusiveMaximum != nil && *c.ExclusiveMaximum
   122  	isExclusiveMinimum := c.ExclusiveMinimum != nil && *c.ExclusiveMinimum
   123  	isUniqueItems := c.UniqueItems != nil && *c.UniqueItems
   124  	pattern := ""
   125  	if c.Pattern != nil {
   126  		pattern = *c.Pattern
   127  	}
   128  
   129  	var transformedItems *spec.SchemaOrArray
   130  	var transformedProperties map[string]spec.Schema
   131  	var transformedAdditionalProperties *spec.SchemaOrBool
   132  
   133  	if c.Items != nil {
   134  		items, err := c.Items.ValidationSchema()
   135  		if err != nil {
   136  			return nil, fmt.Errorf("failed to transform items: %w", err)
   137  		}
   138  		transformedItems = &spec.SchemaOrArray{Schema: items}
   139  	}
   140  
   141  	if c.Properties != nil {
   142  		properties := make(map[string]spec.Schema)
   143  		for key, value := range c.Properties {
   144  			property, err := value.ValidationSchema()
   145  			if err != nil {
   146  				return nil, fmt.Errorf("failed to transform property %q: %w", key, err)
   147  			}
   148  			properties[key] = *property
   149  		}
   150  		transformedProperties = properties
   151  	}
   152  
   153  	if c.AdditionalProperties != nil {
   154  		additionalProperties, err := c.AdditionalProperties.ValidationSchema()
   155  		if err != nil {
   156  			return nil, fmt.Errorf("failed to transform additionalProperties: %w", err)
   157  		}
   158  		transformedAdditionalProperties = &spec.SchemaOrBool{Schema: additionalProperties, Allows: true}
   159  	}
   160  
   161  	res := spec.Schema{
   162  		SchemaProps: spec.SchemaProps{
   163  			Nullable:         isNullable,
   164  			Format:           format,
   165  			Maximum:          c.Maximum,
   166  			ExclusiveMaximum: isExclusiveMaximum,
   167  			Minimum:          c.Minimum,
   168  			ExclusiveMinimum: isExclusiveMinimum,
   169  			MaxLength:        c.MaxLength,
   170  			MinLength:        c.MinLength,
   171  			Pattern:          pattern,
   172  			MaxItems:         c.MaxItems,
   173  			MinItems:         c.MinItems,
   174  			UniqueItems:      isUniqueItems,
   175  			MultipleOf:       c.MultipleOf,
   176  			Enum:             c.Enum,
   177  			MaxProperties:    c.MaxProperties,
   178  			MinProperties:    c.MinProperties,
   179  		},
   180  	}
   181  
   182  	if len(c.CEL) > 0 {
   183  		// Convert the CELTag to a map[string]interface{} via JSON
   184  		celTagJSON, err := json.Marshal(c.CEL)
   185  		if err != nil {
   186  			return nil, fmt.Errorf("failed to marshal CEL tag: %w", err)
   187  		}
   188  		var celTagMap []interface{}
   189  		if err := json.Unmarshal(celTagJSON, &celTagMap); err != nil {
   190  			return nil, fmt.Errorf("failed to unmarshal CEL tag: %w", err)
   191  		}
   192  
   193  		res.VendorExtensible.AddExtension("x-kubernetes-validations", celTagMap)
   194  	}
   195  
   196  	// Dont add structural properties directly to this schema. This schema
   197  	// is used only for validation.
   198  	if transformedItems != nil || len(transformedProperties) > 0 || transformedAdditionalProperties != nil {
   199  		res.AllOf = append(res.AllOf, spec.Schema{
   200  			SchemaProps: spec.SchemaProps{
   201  				Items:                transformedItems,
   202  				Properties:           transformedProperties,
   203  				AdditionalProperties: transformedAdditionalProperties,
   204  			},
   205  		})
   206  	}
   207  
   208  	return &res, nil
   209  }
   210  
   211  // validates the parameters in a CommentTags instance. Returns any errors encountered.
   212  func (c commentTags) Validate() error {
   213  
   214  	var err error
   215  
   216  	if c.MinLength != nil && *c.MinLength < 0 {
   217  		err = errors.Join(err, fmt.Errorf("minLength cannot be negative"))
   218  	}
   219  	if c.MaxLength != nil && *c.MaxLength < 0 {
   220  		err = errors.Join(err, fmt.Errorf("maxLength cannot be negative"))
   221  	}
   222  	if c.MinItems != nil && *c.MinItems < 0 {
   223  		err = errors.Join(err, fmt.Errorf("minItems cannot be negative"))
   224  	}
   225  	if c.MaxItems != nil && *c.MaxItems < 0 {
   226  		err = errors.Join(err, fmt.Errorf("maxItems cannot be negative"))
   227  	}
   228  	if c.MinProperties != nil && *c.MinProperties < 0 {
   229  		err = errors.Join(err, fmt.Errorf("minProperties cannot be negative"))
   230  	}
   231  	if c.MaxProperties != nil && *c.MaxProperties < 0 {
   232  		err = errors.Join(err, fmt.Errorf("maxProperties cannot be negative"))
   233  	}
   234  	if c.Minimum != nil && c.Maximum != nil && *c.Minimum > *c.Maximum {
   235  		err = errors.Join(err, fmt.Errorf("minimum %f is greater than maximum %f", *c.Minimum, *c.Maximum))
   236  	}
   237  	if (c.ExclusiveMinimum != nil || c.ExclusiveMaximum != nil) && c.Minimum != nil && c.Maximum != nil && *c.Minimum == *c.Maximum {
   238  		err = errors.Join(err, fmt.Errorf("exclusiveMinimum/Maximum cannot be set when minimum == maximum"))
   239  	}
   240  	if c.MinLength != nil && c.MaxLength != nil && *c.MinLength > *c.MaxLength {
   241  		err = errors.Join(err, fmt.Errorf("minLength %d is greater than maxLength %d", *c.MinLength, *c.MaxLength))
   242  	}
   243  	if c.MinItems != nil && c.MaxItems != nil && *c.MinItems > *c.MaxItems {
   244  		err = errors.Join(err, fmt.Errorf("minItems %d is greater than maxItems %d", *c.MinItems, *c.MaxItems))
   245  	}
   246  	if c.MinProperties != nil && c.MaxProperties != nil && *c.MinProperties > *c.MaxProperties {
   247  		err = errors.Join(err, fmt.Errorf("minProperties %d is greater than maxProperties %d", *c.MinProperties, *c.MaxProperties))
   248  	}
   249  	if c.Pattern != nil {
   250  		_, e := regexp.Compile(*c.Pattern)
   251  		if e != nil {
   252  			err = errors.Join(err, fmt.Errorf("invalid pattern %q: %v", *c.Pattern, e))
   253  		}
   254  	}
   255  	if c.MultipleOf != nil && *c.MultipleOf == 0 {
   256  		err = errors.Join(err, fmt.Errorf("multipleOf cannot be 0"))
   257  	}
   258  
   259  	for i, celTag := range c.CEL {
   260  		celError := celTag.Validate()
   261  		if celError == nil {
   262  			continue
   263  		}
   264  		err = errors.Join(err, fmt.Errorf("invalid CEL tag at index %d: %w", i, celError))
   265  	}
   266  
   267  	return err
   268  }
   269  
   270  // Performs type-specific validation for CommentTags porameters. Accepts a Type instance and returns any errors encountered during validation.
   271  func (c commentTags) ValidateType(t *types.Type) error {
   272  	var err error
   273  
   274  	resolvedType := resolveAliasAndPtrType(t)
   275  	typeString, _ := openapi.OpenAPITypeFormat(resolvedType.String()) // will be empty for complicated types
   276  
   277  	// Structs and interfaces may dynamically be any type, so we cant validate them
   278  	// easily.
   279  	if resolvedType.Kind == types.Interface || resolvedType.Kind == types.Struct {
   280  		// Skip validation for structs and interfaces which implement custom
   281  		// overrides
   282  		//
   283  		// Only check top-level t type without resolving alias to mirror generator
   284  		// behavior. Generator only checks the top level type without resolving
   285  		// alias. The `has*Method` functions can be changed to add this behavior in the
   286  		// future if needed.
   287  		elemT := resolvePtrType(t)
   288  		if hasOpenAPIDefinitionMethod(elemT) ||
   289  			hasOpenAPIDefinitionMethods(elemT) ||
   290  			hasOpenAPIV3DefinitionMethod(elemT) ||
   291  			hasOpenAPIV3OneOfMethod(elemT) {
   292  
   293  			return nil
   294  		}
   295  	}
   296  
   297  	isArray := resolvedType.Kind == types.Slice || resolvedType.Kind == types.Array
   298  	isMap := resolvedType.Kind == types.Map
   299  	isString := typeString == "string"
   300  	isInt := typeString == "integer"
   301  	isFloat := typeString == "number"
   302  	isStruct := resolvedType.Kind == types.Struct
   303  
   304  	if c.MaxItems != nil && !isArray {
   305  		err = errors.Join(err, fmt.Errorf("maxItems can only be used on array types"))
   306  	}
   307  	if c.MinItems != nil && !isArray {
   308  		err = errors.Join(err, fmt.Errorf("minItems can only be used on array types"))
   309  	}
   310  	if c.UniqueItems != nil && !isArray {
   311  		err = errors.Join(err, fmt.Errorf("uniqueItems can only be used on array types"))
   312  	}
   313  	if c.MaxProperties != nil && !(isMap || isStruct) {
   314  		err = errors.Join(err, fmt.Errorf("maxProperties can only be used on map types"))
   315  	}
   316  	if c.MinProperties != nil && !(isMap || isStruct) {
   317  		err = errors.Join(err, fmt.Errorf("minProperties can only be used on map types"))
   318  	}
   319  	if c.MinLength != nil && !isString {
   320  		err = errors.Join(err, fmt.Errorf("minLength can only be used on string types"))
   321  	}
   322  	if c.MaxLength != nil && !isString {
   323  		err = errors.Join(err, fmt.Errorf("maxLength can only be used on string types"))
   324  	}
   325  	if c.Pattern != nil && !isString {
   326  		err = errors.Join(err, fmt.Errorf("pattern can only be used on string types"))
   327  	}
   328  	if c.Minimum != nil && !isInt && !isFloat {
   329  		err = errors.Join(err, fmt.Errorf("minimum can only be used on numeric types"))
   330  	}
   331  	if c.Maximum != nil && !isInt && !isFloat {
   332  		err = errors.Join(err, fmt.Errorf("maximum can only be used on numeric types"))
   333  	}
   334  	if c.MultipleOf != nil && !isInt && !isFloat {
   335  		err = errors.Join(err, fmt.Errorf("multipleOf can only be used on numeric types"))
   336  	}
   337  	if c.ExclusiveMinimum != nil && !isInt && !isFloat {
   338  		err = errors.Join(err, fmt.Errorf("exclusiveMinimum can only be used on numeric types"))
   339  	}
   340  	if c.ExclusiveMaximum != nil && !isInt && !isFloat {
   341  		err = errors.Join(err, fmt.Errorf("exclusiveMaximum can only be used on numeric types"))
   342  	}
   343  	if c.AdditionalProperties != nil && !isMap {
   344  		err = errors.Join(err, fmt.Errorf("additionalProperties can only be used on map types"))
   345  
   346  		if err == nil {
   347  			err = errors.Join(err, c.AdditionalProperties.ValidateType(t))
   348  		}
   349  	}
   350  	if c.Items != nil && !isArray {
   351  		err = errors.Join(err, fmt.Errorf("items can only be used on array types"))
   352  
   353  		if err == nil {
   354  			err = errors.Join(err, c.Items.ValidateType(t))
   355  		}
   356  	}
   357  	if c.Properties != nil {
   358  		if !isStruct && !isMap {
   359  			err = errors.Join(err, fmt.Errorf("properties can only be used on struct types"))
   360  		} else if isStruct && err == nil {
   361  			for key, tags := range c.Properties {
   362  				if member := memberWithJSONName(resolvedType, key); member == nil {
   363  					err = errors.Join(err, fmt.Errorf("property used in comment tag %q not found in struct %s", key, resolvedType.String()))
   364  				} else if nestedErr := tags.ValidateType(member.Type); nestedErr != nil {
   365  					err = errors.Join(err, fmt.Errorf("failed to validate property %q: %w", key, nestedErr))
   366  				}
   367  			}
   368  		}
   369  	}
   370  
   371  	return err
   372  }
   373  
   374  func memberWithJSONName(t *types.Type, key string) *types.Member {
   375  	for _, member := range t.Members {
   376  		tags := getJsonTags(&member)
   377  		if len(tags) > 0 && tags[0] == key {
   378  			return &member
   379  		} else if member.Embedded {
   380  			if embeddedMember := memberWithJSONName(member.Type, key); embeddedMember != nil {
   381  				return embeddedMember
   382  			}
   383  		}
   384  	}
   385  	return nil
   386  }
   387  
   388  // Parses the given comments into a CommentTags type. Validates the parsed comment tags, and returns the result.
   389  // Accepts an optional type to validate against, and a prefix to filter out markers not related to validation.
   390  // Accepts a prefix to filter out markers not related to validation.
   391  // Returns any errors encountered while parsing or validating the comment tags.
   392  func ParseCommentTags(t *types.Type, comments []string, prefix string) (*spec.Schema, error) {
   393  
   394  	markers, err := parseMarkers(comments, prefix)
   395  	if err != nil {
   396  		return nil, fmt.Errorf("failed to parse marker comments: %w", err)
   397  	}
   398  	nested, err := nestMarkers(markers)
   399  	if err != nil {
   400  		return nil, fmt.Errorf("invalid marker comments: %w", err)
   401  	}
   402  
   403  	// Parse the map into a CommentTags type by marshalling and unmarshalling
   404  	// as JSON in leiu of an unstructured converter.
   405  	out, err := json.Marshal(nested)
   406  	if err != nil {
   407  		return nil, fmt.Errorf("failed to marshal marker comments: %w", err)
   408  	}
   409  
   410  	var commentTags commentTags
   411  	if err = json.Unmarshal(out, &commentTags); err != nil {
   412  		return nil, fmt.Errorf("failed to unmarshal marker comments: %w", err)
   413  	}
   414  
   415  	// Validate the parsed comment tags
   416  	validationErrors := commentTags.Validate()
   417  
   418  	if t != nil {
   419  		validationErrors = errors.Join(validationErrors, commentTags.ValidateType(t))
   420  	}
   421  
   422  	if validationErrors != nil {
   423  		return nil, fmt.Errorf("invalid marker comments: %w", validationErrors)
   424  	}
   425  
   426  	return commentTags.ValidationSchema()
   427  }
   428  
   429  var (
   430  	allowedKeyCharacterSet = `[:_a-zA-Z0-9\[\]\-]`
   431  	valueEmpty             = regexp.MustCompile(fmt.Sprintf(`^(%s*)$`, allowedKeyCharacterSet))
   432  	valueAssign            = regexp.MustCompile(fmt.Sprintf(`^(%s*)=(.*)$`, allowedKeyCharacterSet))
   433  	valueRawString         = regexp.MustCompile(fmt.Sprintf(`^(%s*)>(.*)$`, allowedKeyCharacterSet))
   434  )
   435  
   436  // extractCommentTags parses comments for lines of the form:
   437  //
   438  //	'marker' + "key=value"
   439  //
   440  //	or to specify truthy boolean keys:
   441  //
   442  //	'marker' + "key"
   443  //
   444  // Values are optional; "" is the default.  A tag can be specified more than
   445  // one time and all values are returned.  Returns a map with an entry for
   446  // for each key and a value.
   447  //
   448  // Similar to version from gengo, but this version support only allows one
   449  // value per key (preferring explicit array indices), supports raw strings
   450  // with concatenation, and limits the usable characters allowed in a key
   451  // (for simpler parsing).
   452  //
   453  // Assignments and empty values have the same syntax as from gengo. Raw strings
   454  // have the syntax:
   455  //
   456  //	'marker' + "key>value"
   457  //	'marker' + "key>value"
   458  //
   459  // Successive usages of the same raw string key results in concatenating each
   460  // line with `\n` in between. It is an error to use `=` to assing to a previously
   461  // assigned key
   462  // (in contrast to types.ExtractCommentTags which allows array-typed
   463  // values to be specified using `=`).
   464  func extractCommentTags(marker string, lines []string) (map[string]string, error) {
   465  	out := map[string]string{}
   466  
   467  	// Used to track the the line immediately prior to the one being iterated.
   468  	// If there was an invalid or ignored line, these values get reset.
   469  	lastKey := ""
   470  	lastIndex := -1
   471  	lastArrayKey := ""
   472  
   473  	var lintErrors []error
   474  
   475  	for _, line := range lines {
   476  		line = strings.Trim(line, " ")
   477  
   478  		// Track the current value of the last vars to use in this loop iteration
   479  		// before they are reset for the next iteration.
   480  		previousKey := lastKey
   481  		previousArrayKey := lastArrayKey
   482  		previousIndex := lastIndex
   483  
   484  		// Make sure last vars gets reset if we `continue`
   485  		lastIndex = -1
   486  		lastArrayKey = ""
   487  		lastKey = ""
   488  
   489  		if len(line) == 0 {
   490  			continue
   491  		} else if !strings.HasPrefix(line, marker) {
   492  			continue
   493  		}
   494  
   495  		line = strings.TrimPrefix(line, marker)
   496  
   497  		key := ""
   498  		value := ""
   499  
   500  		if matches := valueAssign.FindStringSubmatch(line); matches != nil {
   501  			key = matches[1]
   502  			value = matches[2]
   503  
   504  			// If key exists, throw error.
   505  			// Some of the old kube open-api gen marker comments like
   506  			// `+listMapKeys` allowed a list to be specified by writing key=value
   507  			// multiple times.
   508  			//
   509  			// This is not longer supported for the prefixed marker comments.
   510  			// This is to prevent confusion with the new array syntax which
   511  			// supports lists of objects.
   512  			//
   513  			// The old marker comments like +listMapKeys will remain functional,
   514  			// but new markers will not support it.
   515  			if _, ok := out[key]; ok {
   516  				return nil, fmt.Errorf("cannot have multiple values for key '%v'", key)
   517  			}
   518  
   519  		} else if matches := valueEmpty.FindStringSubmatch(line); matches != nil {
   520  			key = matches[1]
   521  			value = ""
   522  
   523  		} else if matches := valueRawString.FindStringSubmatch(line); matches != nil {
   524  			toAdd := strings.Trim(string(matches[2]), " ")
   525  
   526  			key = matches[1]
   527  
   528  			// First usage as a raw string.
   529  			if existing, exists := out[key]; !exists {
   530  
   531  				// Encode the raw string as JSON to ensure that it is properly escaped.
   532  				valueBytes, err := json.Marshal(toAdd)
   533  				if err != nil {
   534  					return nil, fmt.Errorf("invalid value for key %v: %w", key, err)
   535  				}
   536  
   537  				value = string(valueBytes)
   538  			} else if key != previousKey {
   539  				// Successive usages of the same key of a raw string must be
   540  				// consecutive
   541  				return nil, fmt.Errorf("concatenations to key '%s' must be consecutive with its assignment", key)
   542  			} else {
   543  				// If it is a consecutive repeat usage, concatenate to the
   544  				// existing value.
   545  				//
   546  				// Decode JSON string, append to it, re-encode JSON string.
   547  				// Kinda janky but this is a code-generator...
   548  				var unmarshalled string
   549  				if err := json.Unmarshal([]byte(existing), &unmarshalled); err != nil {
   550  					return nil, fmt.Errorf("invalid value for key %v: %w", key, err)
   551  				} else {
   552  					unmarshalled += "\n" + toAdd
   553  					valueBytes, err := json.Marshal(unmarshalled)
   554  					if err != nil {
   555  						return nil, fmt.Errorf("invalid value for key %v: %w", key, err)
   556  					}
   557  
   558  					value = string(valueBytes)
   559  				}
   560  			}
   561  		} else {
   562  			// Comment has the correct prefix, but incorrect syntax, so it is an
   563  			// error
   564  			return nil, fmt.Errorf("invalid marker comment does not match expected `+key=<json formatted value>` pattern: %v", line)
   565  		}
   566  
   567  		out[key] = value
   568  		lastKey = key
   569  
   570  		// Lint the array subscript for common mistakes. This only lints the last
   571  		// array index used, (since we do not have a need for nested arrays yet
   572  		// in markers)
   573  		if arrayPath, index, hasSubscript, err := extractArraySubscript(key); hasSubscript {
   574  			// If index is non-zero, check that that previous line was for the same
   575  			// key and either the same or previous index
   576  			if err != nil {
   577  				lintErrors = append(lintErrors, fmt.Errorf("error parsing %v: expected integer index in key '%v'", line, key))
   578  			} else if previousArrayKey != arrayPath && index != 0 {
   579  				lintErrors = append(lintErrors, fmt.Errorf("error parsing %v: non-consecutive index %v for key '%v'", line, index, arrayPath))
   580  			} else if index != previousIndex+1 && index != previousIndex {
   581  				lintErrors = append(lintErrors, fmt.Errorf("error parsing %v: non-consecutive index %v for key '%v'", line, index, arrayPath))
   582  			}
   583  
   584  			lastIndex = index
   585  			lastArrayKey = arrayPath
   586  		}
   587  	}
   588  
   589  	if len(lintErrors) > 0 {
   590  		return nil, errors.Join(lintErrors...)
   591  	}
   592  
   593  	return out, nil
   594  }
   595  
   596  // Extracts and parses the given marker comments into a map of key -> value.
   597  // Accepts a prefix to filter out markers not related to validation.
   598  // The prefix is removed from the key in the returned map.
   599  // Empty keys and invalid values will return errors, refs are currently unsupported and will be skipped.
   600  func parseMarkers(markerComments []string, prefix string) (map[string]any, error) {
   601  	markers, err := extractCommentTags(prefix, markerComments)
   602  	if err != nil {
   603  		return nil, err
   604  	}
   605  
   606  	// Parse the values as JSON
   607  	result := map[string]any{}
   608  	for key, value := range markers {
   609  		var unmarshalled interface{}
   610  
   611  		if len(key) == 0 {
   612  			return nil, fmt.Errorf("cannot have empty key for marker comment")
   613  		} else if _, ok := parseSymbolReference(value, ""); ok {
   614  			// Skip ref markers
   615  			continue
   616  		} else if len(value) == 0 {
   617  			// Empty value means key is implicitly a bool
   618  			result[key] = true
   619  		} else if err := json.Unmarshal([]byte(value), &unmarshalled); err != nil {
   620  			// Not valid JSON, throw error
   621  			return nil, fmt.Errorf("failed to parse value for key %v as JSON: %w", key, err)
   622  		} else {
   623  			// Is is valid JSON, use as a JSON value
   624  			result[key] = unmarshalled
   625  		}
   626  	}
   627  	return result, nil
   628  }
   629  
   630  // Converts a map of:
   631  //
   632  //	"a:b:c": 1
   633  //	"a:b:d": 2
   634  //	"a:e": 3
   635  //	"f": 4
   636  //
   637  // Into:
   638  //
   639  //	 map[string]any{
   640  //	   "a": map[string]any{
   641  //		      "b": map[string]any{
   642  //		          "c": 1,
   643  //				  "d": 2,
   644  //			   },
   645  //			   "e": 3,
   646  //		  },
   647  //		  "f": 4,
   648  //	 }
   649  //
   650  // Returns a list of joined errors for any invalid keys. See putNestedValue for more details.
   651  func nestMarkers(markers map[string]any) (map[string]any, error) {
   652  	nested := make(map[string]any)
   653  	var errs []error
   654  	for key, value := range markers {
   655  		var err error
   656  		keys := strings.Split(key, ":")
   657  
   658  		if err = putNestedValue(nested, keys, value); err != nil {
   659  			errs = append(errs, err)
   660  		}
   661  	}
   662  
   663  	if len(errs) > 0 {
   664  		return nil, errors.Join(errs...)
   665  	}
   666  
   667  	return nested, nil
   668  }
   669  
   670  // Recursively puts a value into the given keypath, creating intermediate maps
   671  // and slices as needed. If a key is of the form `foo[bar]`, then bar will be
   672  // treated as an index into the array foo. If bar is not a valid integer, putNestedValue returns an error.
   673  func putNestedValue(m map[string]any, k []string, v any) error {
   674  	if len(k) == 0 {
   675  		return nil
   676  	}
   677  
   678  	key := k[0]
   679  	rest := k[1:]
   680  
   681  	// Array case
   682  	if arrayKeyWithoutSubscript, index, hasSubscript, err := extractArraySubscript(key); err != nil {
   683  		return fmt.Errorf("error parsing subscript for key %v: %w", key, err)
   684  	} else if hasSubscript {
   685  		key = arrayKeyWithoutSubscript
   686  		var arrayDestination []any
   687  		if existing, ok := m[key]; !ok {
   688  			arrayDestination = make([]any, index+1)
   689  		} else if existing, ok := existing.([]any); !ok {
   690  			// Error case. Existing isn't of correct type. Can happen if
   691  			// someone is subscripting a field that was previously not an array
   692  			return fmt.Errorf("expected []any at key %v, got %T", key, existing)
   693  		} else if index >= len(existing) {
   694  			// Ensure array is big enough
   695  			arrayDestination = append(existing, make([]any, index-len(existing)+1)...)
   696  		} else {
   697  			arrayDestination = existing
   698  		}
   699  
   700  		m[key] = arrayDestination
   701  		if arrayDestination[index] == nil {
   702  			// Doesn't exist case, create the destination.
   703  			// Assumes the destination is a map for now. Theoretically could be
   704  			// extended to support arrays of arrays, but that's not needed yet.
   705  			destination := make(map[string]any)
   706  			arrayDestination[index] = destination
   707  			if err = putNestedValue(destination, rest, v); err != nil {
   708  				return err
   709  			}
   710  		} else if dst, ok := arrayDestination[index].(map[string]any); ok {
   711  			// Already exists case, correct type
   712  			if putNestedValue(dst, rest, v); err != nil {
   713  				return err
   714  			}
   715  		} else {
   716  			// Already exists, incorrect type. Error
   717  			// This shouldn't be possible.
   718  			return fmt.Errorf("expected map at %v[%v], got %T", key, index, arrayDestination[index])
   719  		}
   720  
   721  		return nil
   722  	} else if len(rest) == 0 {
   723  		// Base case. Single key. Just set into destination
   724  		m[key] = v
   725  		return nil
   726  	}
   727  
   728  	if existing, ok := m[key]; !ok {
   729  		destination := make(map[string]any)
   730  		m[key] = destination
   731  		return putNestedValue(destination, rest, v)
   732  	} else if destination, ok := existing.(map[string]any); ok {
   733  		return putNestedValue(destination, rest, v)
   734  	} else {
   735  		// Error case. Existing isn't of correct type. Can happen if prior comment
   736  		// referred to value as an error
   737  		return fmt.Errorf("expected map[string]any at key %v, got %T", key, existing)
   738  	}
   739  }
   740  
   741  // extractArraySubscript extracts the left array subscript from a key of
   742  // the form  `foo[bar][baz]` -> "bar".
   743  // Returns the key without the subscript, the index, and a bool indicating if
   744  // the key had a subscript.
   745  // If the key has a subscript, but the subscript is not a valid integer, returns an error.
   746  //
   747  // This can be adapted to support multidimensional subscripts probably fairly
   748  // easily by retuning a list of ints
   749  func extractArraySubscript(str string) (string, int, bool, error) {
   750  	subscriptIdx := strings.Index(str, "[")
   751  	if subscriptIdx == -1 {
   752  		return "", -1, false, nil
   753  	}
   754  
   755  	subscript := strings.Split(str[subscriptIdx+1:], "]")[0]
   756  	if len(subscript) == 0 {
   757  		return "", -1, false, fmt.Errorf("empty subscript not allowed")
   758  	}
   759  
   760  	index, err := strconv.Atoi(subscript)
   761  	if err != nil {
   762  		return "", -1, false, fmt.Errorf("expected integer index in key %v", str)
   763  	} else if index < 0 {
   764  		return "", -1, false, fmt.Errorf("subscript '%v' is invalid. index must be positive", subscript)
   765  	}
   766  
   767  	return str[:subscriptIdx], index, true, nil
   768  }