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 }