github.com/Jeffail/benthos/v3@v3.65.0/internal/docs/yaml.go (about) 1 package docs 2 3 import ( 4 "fmt" 5 6 "gopkg.in/yaml.v3" 7 ) 8 9 // FieldsFromYAML walks the children of a YAML node and returns a list of fields 10 // extracted from it. This can be used in order to infer a field spec for a 11 // parsed component. 12 func FieldsFromYAML(node *yaml.Node) FieldSpecs { 13 node = unwrapDocumentNode(node) 14 15 var fields FieldSpecs 16 for i := 0; i < len(node.Content)-1; i += 2 { 17 fields = append(fields, FieldFromYAML(node.Content[i].Value, node.Content[i+1])) 18 } 19 return fields 20 } 21 22 // FieldFromYAML infers a field spec from a YAML node. This mechanism has many 23 // limitations and should only be used for pre-hydrating field specs for old 24 // components with struct based config. 25 func FieldFromYAML(name string, node *yaml.Node) FieldSpec { 26 node = unwrapDocumentNode(node) 27 28 field := FieldCommon(name, "") 29 30 switch node.Kind { 31 case yaml.MappingNode: 32 field = field.WithChildren(FieldsFromYAML(node)...) 33 field.Type = FieldTypeObject 34 if len(field.Children) == 0 { 35 var defaultI interface{} = map[string]interface{}{} 36 field.Default = &defaultI 37 } 38 case yaml.SequenceNode: 39 field.Kind = KindArray 40 field.Type = FieldTypeUnknown 41 if len(node.Content) > 0 { 42 tmpField := FieldFromYAML("", node.Content[0]) 43 field.Type = tmpField.Type 44 field.Children = tmpField.Children 45 switch field.Type { 46 case FieldTypeString: 47 var defaultArray []string 48 _ = node.Decode(&defaultArray) 49 50 var defaultI interface{} = defaultArray 51 field.Default = &defaultI 52 case FieldTypeInt: 53 var defaultArray []int64 54 _ = node.Decode(&defaultArray) 55 56 var defaultI interface{} = defaultArray 57 field.Default = &defaultI 58 } 59 } else { 60 var defaultI interface{} = []interface{}{} 61 field.Default = &defaultI 62 } 63 case yaml.ScalarNode: 64 switch node.Tag { 65 case "!!bool": 66 field.Type = FieldTypeBool 67 68 var defaultBool bool 69 _ = node.Decode(&defaultBool) 70 71 var defaultI interface{} = defaultBool 72 field.Default = &defaultI 73 case "!!int": 74 field.Type = FieldTypeInt 75 76 var defaultInt int64 77 _ = node.Decode(&defaultInt) 78 79 var defaultI interface{} = defaultInt 80 field.Default = &defaultI 81 case "!!float": 82 field.Type = FieldTypeFloat 83 84 var defaultFloat float64 85 _ = node.Decode(&defaultFloat) 86 87 var defaultI interface{} = defaultFloat 88 field.Default = &defaultI 89 default: 90 field.Type = FieldTypeString 91 92 var defaultStr string 93 _ = node.Decode(&defaultStr) 94 95 var defaultI interface{} = defaultStr 96 field.Default = &defaultI 97 } 98 } 99 100 return field 101 } 102 103 // GetInferenceCandidateFromYAML checks a yaml node config structure for a 104 // component and returns either the inferred type name or an error if one cannot 105 // be inferred. 106 func GetInferenceCandidateFromYAML(docProv Provider, t Type, defaultType string, node *yaml.Node) (string, ComponentSpec, error) { 107 if docProv == nil { 108 docProv = globalProvider 109 } 110 111 node = unwrapDocumentNode(node) 112 113 if node.Kind != yaml.MappingNode { 114 return "", ComponentSpec{}, fmt.Errorf("invalid type %v, expected object", node.Kind) 115 } 116 117 var keys []string 118 for i := 0; i < len(node.Content)-1; i += 2 { 119 if node.Content[i].Value == "type" { 120 tStr := node.Content[i+1].Value 121 spec, exists := GetDocs(docProv, tStr, t) 122 if !exists { 123 return "", ComponentSpec{}, fmt.Errorf("%v type '%v' was not recognised", string(t), tStr) 124 } 125 return tStr, spec, nil 126 } 127 keys = append(keys, node.Content[i].Value) 128 } 129 130 return getInferenceCandidateFromList(docProv, t, defaultType, keys) 131 } 132 133 // GetPluginConfigYAML extracts a plugin configuration node from a component 134 // config. This exists because there are two styles of plugin config, the old 135 // style (within `plugin`): 136 // 137 // type: foo 138 // plugin: 139 // bar: baz 140 // 141 // And the new style: 142 // 143 // foo: 144 // bar: baz 145 // 146 func GetPluginConfigYAML(name string, node *yaml.Node) (yaml.Node, error) { 147 node = unwrapDocumentNode(node) 148 for i := 0; i < len(node.Content)-1; i += 2 { 149 if node.Content[i].Value == name { 150 return *node.Content[i+1], nil 151 } 152 } 153 pluginStruct := struct { 154 Plugin yaml.Node `yaml:"plugin"` 155 }{} 156 if err := node.Decode(&pluginStruct); err != nil { 157 return yaml.Node{}, err 158 } 159 return pluginStruct.Plugin, nil 160 } 161 162 //------------------------------------------------------------------------------ 163 164 func (f FieldSpec) shouldOmitYAML(parentFields FieldSpecs, fieldNode, parentNode *yaml.Node) (why string, shouldOmit bool) { 165 conf := ToValueConfig{ 166 Passive: true, 167 FallbackToInterface: true, 168 } 169 170 if f.omitWhenFn == nil { 171 return 172 } 173 field, err := f.YAMLToValue(fieldNode, conf) 174 if err != nil { 175 // If we weren't able to infer a value type then it's assumed 176 // that we'll capture this type error elsewhere. 177 return 178 } 179 parent, err := parentFields.YAMLToMap(parentNode, conf) 180 if err != nil { 181 // If we weren't able to infer a value type then it's assumed 182 // that we'll capture this type error elsewhere. 183 return 184 } 185 return f.omitWhenFn(field, parent) 186 } 187 188 // TODO: V4 Remove this. 189 func sanitiseConditionConfigYAML(node *yaml.Node) error { 190 // This is a nasty hack until Benthos v4. 191 newNodes := []*yaml.Node{} 192 193 var name string 194 for i := 0; i < len(node.Content)-1; i += 2 { 195 if node.Content[i].Value == "type" { 196 name = node.Content[i+1].Value 197 newNodes = append(newNodes, node.Content[i], node.Content[i+1]) 198 break 199 } 200 } 201 202 if name == "" { 203 return nil 204 } 205 206 for i := 0; i < len(node.Content)-1; i += 2 { 207 if node.Content[i].Value == name { 208 newNodes = append(newNodes, node.Content[i], node.Content[i+1]) 209 break 210 } 211 } 212 213 node.Content = newNodes 214 return nil 215 } 216 217 // SanitiseYAML takes a yaml.Node and a config spec and sorts the fields of the 218 // node according to the spec. Also optionally removes the `type` field from 219 // this and all nested components. 220 func SanitiseYAML(cType Type, node *yaml.Node, conf SanitiseConfig) error { 221 node = unwrapDocumentNode(node) 222 223 if cType == "condition" { 224 return sanitiseConditionConfigYAML(node) 225 } 226 227 newNodes := []*yaml.Node{} 228 229 var name string 230 var keys []string 231 for i := 0; i < len(node.Content)-1; i += 2 { 232 if node.Content[i].Value == "label" { 233 if _, omit := labelField.shouldOmitYAML(nil, node.Content[i+1], node); !omit { 234 newNodes = append(newNodes, node.Content[i], node.Content[i+1]) 235 } 236 break 237 } 238 } 239 for i := 0; i < len(node.Content)-1; i += 2 { 240 if node.Content[i].Value == "type" { 241 name = node.Content[i+1].Value 242 if !conf.RemoveTypeField { 243 newNodes = append(newNodes, node.Content[i], node.Content[i+1]) 244 } 245 break 246 } else { 247 keys = append(keys, node.Content[i].Value) 248 } 249 } 250 if name == "" { 251 if len(node.Content) == 0 { 252 return nil 253 } 254 var err error 255 if name, _, err = getInferenceCandidateFromList(conf, cType, "", keys); err != nil { 256 return err 257 } 258 } 259 260 cSpec, exists := GetDocs(conf, name, cType) 261 if !exists { 262 return fmt.Errorf("failed to obtain docs for %v type %v", cType, name) 263 } 264 265 nameFound := false 266 for i := 0; i < len(node.Content)-1; i += 2 { 267 if node.Content[i].Value == "plugin" && cSpec.Plugin { 268 node.Content[i].Value = name 269 } 270 271 if node.Content[i].Value != name { 272 continue 273 } 274 275 nameFound = true 276 if err := cSpec.Config.SanitiseYAML(node.Content[i+1], conf); err != nil { 277 return err 278 } 279 newNodes = append(newNodes, node.Content[i], node.Content[i+1]) 280 break 281 } 282 283 // If the type field was omitted but we didn't see a config under the name 284 // then we need to add an empty object. 285 if !nameFound && conf.RemoveTypeField { 286 var keyNode yaml.Node 287 if err := keyNode.Encode(name); err != nil { 288 return err 289 } 290 bodyNode, err := cSpec.Config.ToYAML(conf.ForExample) 291 if err != nil { 292 return err 293 } 294 if err := cSpec.Config.SanitiseYAML(bodyNode, conf); err != nil { 295 return err 296 } 297 newNodes = append(newNodes, &keyNode, bodyNode) 298 } 299 300 reservedFields := reservedFieldsByType(cType) 301 for i := 0; i < len(node.Content)-1; i += 2 { 302 if node.Content[i].Value == name || node.Content[i].Value == "type" || node.Content[i].Value == "label" { 303 continue 304 } 305 if spec, exists := reservedFields[node.Content[i].Value]; exists { 306 if _, omit := spec.shouldOmitYAML(nil, node.Content[i+1], node); omit { 307 continue 308 } 309 if err := spec.SanitiseYAML(node.Content[i+1], conf); err != nil { 310 return err 311 } 312 newNodes = append(newNodes, node.Content[i], node.Content[i+1]) 313 } 314 } 315 316 node.Content = newNodes 317 return nil 318 } 319 320 // SanitiseYAML attempts to reduce a parsed config (as a *yaml.Node) down into a 321 // minimal representation without changing the behaviour of the config. The 322 // fields of the result will also be sorted according to the field spec. 323 func (f FieldSpec) SanitiseYAML(node *yaml.Node, conf SanitiseConfig) error { 324 node = unwrapDocumentNode(node) 325 326 if coreType, isCore := f.Type.IsCoreComponent(); isCore { 327 switch f.Kind { 328 case Kind2DArray: 329 for i := 0; i < len(node.Content); i++ { 330 for j := 0; j < len(node.Content[i].Content); j++ { 331 if err := SanitiseYAML(coreType, node.Content[i].Content[j], conf); err != nil { 332 return err 333 } 334 } 335 } 336 case KindArray: 337 for i := 0; i < len(node.Content); i++ { 338 if err := SanitiseYAML(coreType, node.Content[i], conf); err != nil { 339 return err 340 } 341 } 342 case KindMap: 343 for i := 0; i < len(node.Content)-1; i += 2 { 344 if err := SanitiseYAML(coreType, node.Content[i+1], conf); err != nil { 345 return err 346 } 347 } 348 default: 349 if err := SanitiseYAML(coreType, node, conf); err != nil { 350 return err 351 } 352 } 353 } else if len(f.Children) > 0 { 354 switch f.Kind { 355 case Kind2DArray: 356 for i := 0; i < len(node.Content); i++ { 357 for j := 0; j < len(node.Content[i].Content); j++ { 358 if err := f.Children.SanitiseYAML(node.Content[i].Content[j], conf); err != nil { 359 return err 360 } 361 } 362 } 363 case KindArray: 364 for i := 0; i < len(node.Content); i++ { 365 if err := f.Children.SanitiseYAML(node.Content[i], conf); err != nil { 366 return err 367 } 368 } 369 case KindMap: 370 for i := 0; i < len(node.Content)-1; i += 2 { 371 if err := f.Children.SanitiseYAML(node.Content[i+1], conf); err != nil { 372 return err 373 } 374 } 375 default: 376 if err := f.Children.SanitiseYAML(node, conf); err != nil { 377 return err 378 } 379 } 380 } 381 return nil 382 } 383 384 // SanitiseYAML attempts to reduce a parsed config (as a *yaml.Node) down into a 385 // minimal representation without changing the behaviour of the config. The 386 // fields of the result will also be sorted according to the field spec. 387 func (f FieldSpecs) SanitiseYAML(node *yaml.Node, conf SanitiseConfig) error { 388 node = unwrapDocumentNode(node) 389 390 nodeKeys := map[string]*yaml.Node{} 391 for i := 0; i < len(node.Content)-1; i += 2 { 392 nodeKeys[node.Content[i].Value] = node.Content[i+1] 393 } 394 395 // Following the order of our field specs, extract each field. 396 newNodes := []*yaml.Node{} 397 for _, field := range f { 398 if field.IsDeprecated && conf.RemoveDeprecated { 399 continue 400 } 401 if conf.Filter.shouldDrop(field) { 402 continue 403 } 404 value, exists := nodeKeys[field.Name] 405 if !exists { 406 continue 407 } 408 if _, omit := field.shouldOmitYAML(f, value, node); omit { 409 continue 410 } 411 if err := field.SanitiseYAML(value, conf); err != nil { 412 return err 413 } 414 var keyNode yaml.Node 415 if err := keyNode.Encode(field.Name); err != nil { 416 return err 417 } 418 newNodes = append(newNodes, &keyNode, value) 419 } 420 node.Content = newNodes 421 return nil 422 } 423 424 //------------------------------------------------------------------------------ 425 426 func lintYAMLFromOmit(parentSpec FieldSpecs, lintTargetSpec FieldSpec, parent, node *yaml.Node) []Lint { 427 why, shouldOmit := lintTargetSpec.shouldOmitYAML(parentSpec, node, parent) 428 if shouldOmit { 429 return []Lint{NewLintError(node.Line, why)} 430 } 431 return nil 432 } 433 434 func customLintFromYAML(ctx LintContext, spec FieldSpec, node *yaml.Node) []Lint { 435 lintFn := spec.GetLintFunc() 436 if lintFn == nil { 437 return nil 438 } 439 fieldValue, err := spec.YAMLToValue(node, ToValueConfig{ 440 Passive: true, 441 FallbackToInterface: true, 442 }) 443 if err != nil { 444 // If we weren't able to infer a value type then it's assumed 445 // that we'll capture this type error elsewhere. 446 return []Lint{} 447 } 448 line := node.Line 449 if node.Style == yaml.LiteralStyle { 450 line++ 451 } 452 453 lints := lintFn(ctx, line, node.Column, fieldValue) 454 return lints 455 } 456 457 // LintYAML takes a yaml.Node and a config spec and returns a list of linting 458 // errors found in the config. 459 func LintYAML(ctx LintContext, cType Type, node *yaml.Node) []Lint { 460 if cType == "condition" { 461 if ctx.RejectDeprecated { 462 return []Lint{ 463 NewLintError(node.Line, "condition components are deprecated, use bloblang mappings instead when `check` fields or other alternatives are available"), 464 } 465 } 466 return nil 467 } 468 469 node = unwrapDocumentNode(node) 470 471 var lints []Lint 472 473 var name string 474 var keys []string 475 for i := 0; i < len(node.Content)-1; i += 2 { 476 if node.Content[i].Value == "type" { 477 name = node.Content[i+1].Value 478 break 479 } else { 480 keys = append(keys, node.Content[i].Value) 481 } 482 } 483 if name == "" { 484 if len(node.Content) == 0 { 485 return nil 486 } 487 var err error 488 if name, _, err = getInferenceCandidateFromList(ctx.DocsProvider, cType, "", keys); err != nil { 489 lints = append(lints, NewLintWarning(node.Line, "unable to infer component type")) 490 return lints 491 } 492 } 493 494 cSpec, exists := GetDocs(ctx.DocsProvider, name, cType) 495 if !exists { 496 lints = append(lints, NewLintWarning(node.Line, fmt.Sprintf("failed to obtain docs for %v type %v", cType, name))) 497 return lints 498 } 499 500 if ctx.RejectDeprecated && cSpec.Status == StatusDeprecated { 501 lints = append(lints, NewLintError(node.Line, fmt.Sprintf("component %v is deprecated", cSpec.Name))) 502 } 503 504 nameFound := false 505 for i := 0; i < len(node.Content)-1; i += 2 { 506 if node.Content[i].Value == name { 507 nameFound = true 508 lints = append(lints, cSpec.Config.LintYAML(ctx, node.Content[i+1])...) 509 break 510 } 511 } 512 513 reservedFields := reservedFieldsByType(cType) 514 for i := 0; i < len(node.Content)-1; i += 2 { 515 if node.Content[i].Value == name || node.Content[i].Value == "type" { 516 continue 517 } 518 if node.Content[i].Value == "plugin" { 519 if nameFound || !cSpec.Plugin { 520 lints = append(lints, NewLintError(node.Content[i].Line, "plugin object is ineffective")) 521 } else { 522 lints = append(lints, cSpec.Config.LintYAML(ctx, node.Content[i+1])...) 523 } 524 } 525 spec, exists := reservedFields[node.Content[i].Value] 526 if exists { 527 lints = append(lints, lintYAMLFromOmit(cSpec.Config.Children, spec, node, node.Content[i+1])...) 528 lints = append(lints, spec.LintYAML(ctx, node.Content[i+1])...) 529 } else { 530 lints = append(lints, NewLintError( 531 node.Content[i].Line, 532 fmt.Sprintf("field %v is invalid when the component type is %v (%v)", node.Content[i].Value, name, cType), 533 )) 534 } 535 } 536 537 return lints 538 } 539 540 // LintYAML returns a list of linting errors found by checking a field 541 // definition against a yaml node. 542 func (f FieldSpec) LintYAML(ctx LintContext, node *yaml.Node) []Lint { 543 if f.skipLint { 544 return nil 545 } 546 547 node = unwrapDocumentNode(node) 548 549 var lints []Lint 550 551 if ctx.RejectDeprecated && f.IsDeprecated { 552 lints = append(lints, NewLintError(node.Line, fmt.Sprintf("field %v is deprecated", f.Name))) 553 } 554 555 // Execute custom linters, if the kind is non-scalar this means we execute 556 // the linter from the perspective of both the scalar and higher level types 557 // and it's up to the linting implementation to distinguish between them. 558 lints = append(lints, customLintFromYAML(ctx, f, node)...) 559 560 // Check basic kind matches, and execute custom linters 561 switch f.Kind { 562 case Kind2DArray: 563 if node.Kind != yaml.SequenceNode { 564 lints = append(lints, NewLintError(node.Line, "expected array value")) 565 return lints 566 } 567 for i := 0; i < len(node.Content); i++ { 568 lints = append(lints, f.Array().LintYAML(ctx, node.Content[i])...) 569 } 570 return lints 571 case KindArray: 572 if node.Kind != yaml.SequenceNode { 573 lints = append(lints, NewLintError(node.Line, "expected array value")) 574 return lints 575 } 576 for i := 0; i < len(node.Content); i++ { 577 lints = append(lints, f.Scalar().LintYAML(ctx, node.Content[i])...) 578 } 579 return lints 580 case KindMap: 581 if node.Kind != yaml.MappingNode { 582 lints = append(lints, NewLintError(node.Line, "expected object value")) 583 return lints 584 } 585 for i := 0; i < len(node.Content)-1; i += 2 { 586 lints = append(lints, f.Scalar().LintYAML(ctx, node.Content[i+1])...) 587 } 588 return lints 589 } 590 591 // If we're a core type then execute component specific linting 592 if coreType, isCore := f.Type.IsCoreComponent(); isCore { 593 return append(lints, LintYAML(ctx, coreType, node)...) 594 } 595 596 // If the field has children then lint the child fields 597 if len(f.Children) > 0 { 598 return append(lints, f.Children.LintYAML(ctx, node)...) 599 } 600 601 // Otherwise we're a leaf node, so do basic type checking 602 switch f.Type { 603 // TODO: Do proper checking for bool and number types. 604 case FieldTypeBool, FieldTypeString, FieldTypeInt, FieldTypeFloat: 605 if node.Kind == yaml.MappingNode || node.Kind == yaml.SequenceNode { 606 lints = append(lints, NewLintError(node.Line, fmt.Sprintf("expected %v value", f.Type))) 607 } 608 case FieldTypeObject: 609 if node.Kind != yaml.MappingNode && node.Kind != yaml.AliasNode { 610 lints = append(lints, NewLintError(node.Line, "expected object value")) 611 } 612 } 613 return lints 614 } 615 616 // LintYAML walks a yaml node and returns a list of linting errors found. 617 func (f FieldSpecs) LintYAML(ctx LintContext, node *yaml.Node) []Lint { 618 node = unwrapDocumentNode(node) 619 620 var lints []Lint 621 if node.Kind != yaml.MappingNode { 622 if node.Kind == yaml.AliasNode { 623 // TODO: Actually lint through aliases 624 return nil 625 } 626 lints = append(lints, NewLintError(node.Line, "expected object value")) 627 return lints 628 } 629 630 specNames := map[string]FieldSpec{} 631 for _, field := range f { 632 specNames[field.Name] = field 633 } 634 635 for i := 0; i < len(node.Content)-1; i += 2 { 636 spec, exists := specNames[node.Content[i].Value] 637 if !exists { 638 if node.Content[i+1].Kind != yaml.AliasNode { 639 lints = append(lints, NewLintError(node.Content[i].Line, fmt.Sprintf("field %v not recognised", node.Content[i].Value))) 640 } 641 continue 642 } 643 lints = append(lints, lintYAMLFromOmit(f, spec, node, node.Content[i+1])...) 644 lints = append(lints, spec.LintYAML(ctx, node.Content[i+1])...) 645 delete(specNames, node.Content[i].Value) 646 } 647 648 for name, remaining := range specNames { 649 _, isCore := remaining.Type.IsCoreComponent() 650 if remaining.needsDefault() && 651 remaining.Default == nil && 652 !isCore && 653 remaining.Kind == KindScalar && 654 len(remaining.Children) == 0 { 655 lints = append(lints, NewLintError(node.Line, fmt.Sprintf("field %v is required", name))) 656 } 657 } 658 return lints 659 } 660 661 //------------------------------------------------------------------------------ 662 663 // ToYAML creates a YAML node from a field spec. If a default value has been 664 // specified then it is used. Otherwise, a zero value is generated. If recurse 665 // is enabled and the field has children then all children will also have values 666 // generated. 667 func (f FieldSpec) ToYAML(recurse bool) (*yaml.Node, error) { 668 var node yaml.Node 669 if f.Default != nil { 670 if err := node.Encode(*f.Default); err != nil { 671 return nil, err 672 } 673 return &node, nil 674 } 675 if f.Kind == KindArray || f.Kind == Kind2DArray { 676 s := []interface{}{} 677 if err := node.Encode(s); err != nil { 678 return nil, err 679 } 680 } else if f.Kind == KindMap || len(f.Children) > 0 { 681 if len(f.Children) > 0 && recurse { 682 return f.Children.ToYAML() 683 } 684 s := map[string]interface{}{} 685 if err := node.Encode(s); err != nil { 686 return nil, err 687 } 688 } else { 689 switch f.Type { 690 case FieldTypeString: 691 if err := node.Encode(""); err != nil { 692 return nil, err 693 } 694 case FieldTypeInt: 695 if err := node.Encode(0); err != nil { 696 return nil, err 697 } 698 case FieldTypeFloat: 699 if err := node.Encode(0.0); err != nil { 700 return nil, err 701 } 702 case FieldTypeBool: 703 if err := node.Encode(false); err != nil { 704 return nil, err 705 } 706 default: 707 if err := node.Encode(nil); err != nil { 708 return nil, err 709 } 710 } 711 } 712 return &node, nil 713 } 714 715 // ToYAML creates a YAML node from a list of field specs. If a default value has 716 // been specified for a given field then it is used. Otherwise, a zero value is 717 // generated. 718 func (f FieldSpecs) ToYAML() (*yaml.Node, error) { 719 var node yaml.Node 720 node.Kind = yaml.MappingNode 721 722 for _, spec := range f { 723 var keyNode yaml.Node 724 if err := keyNode.Encode(spec.Name); err != nil { 725 return nil, err 726 } 727 valueNode, err := spec.ToYAML(true) 728 if err != nil { 729 return nil, err 730 } 731 node.Content = append(node.Content, &keyNode, valueNode) 732 } 733 734 return &node, nil 735 } 736 737 // ToValueConfig describes custom options for how documentation fields should be 738 // used to convert a parsed node to a value type. 739 type ToValueConfig struct { 740 // Whether an problem in the config node detected during conversion 741 // should return a "best attempt" structure rather than an error. 742 Passive bool 743 744 // When a field spec is for a non-scalar type (a component) fall back to 745 // decoding it into an interface, otherwise the raw yaml.Node is 746 // returned in its place. 747 FallbackToInterface bool 748 } 749 750 // YAMLToValue converts a yaml node into a generic value by referencing the 751 // expected type. 752 func (f FieldSpec) YAMLToValue(node *yaml.Node, conf ToValueConfig) (interface{}, error) { 753 node = unwrapDocumentNode(node) 754 755 switch f.Kind { 756 case Kind2DArray: 757 if !conf.Passive && node.Kind != yaml.SequenceNode { 758 return nil, fmt.Errorf("line %v: expected array value, got %v", node.Line, node.Kind) 759 } 760 subSpec := f.Array() 761 762 var s []interface{} 763 for i := 0; i < len(node.Content); i++ { 764 v, err := subSpec.YAMLToValue(node.Content[i], conf) 765 if err != nil { 766 return nil, err 767 } 768 s = append(s, v) 769 } 770 return s, nil 771 case KindArray: 772 if !conf.Passive && node.Kind != yaml.SequenceNode { 773 return nil, fmt.Errorf("line %v: expected array value, got %v", node.Line, node.Kind) 774 } 775 subSpec := f.Scalar() 776 777 var s []interface{} 778 for i := 0; i < len(node.Content); i++ { 779 v, err := subSpec.YAMLToValue(node.Content[i], conf) 780 if err != nil { 781 return nil, err 782 } 783 s = append(s, v) 784 } 785 return s, nil 786 case KindMap: 787 if !conf.Passive && node.Kind != yaml.MappingNode { 788 return nil, fmt.Errorf("line %v: expected map value, got %v", node.Line, node.Kind) 789 } 790 subSpec := f.Scalar() 791 792 m := map[string]interface{}{} 793 for i := 0; i < len(node.Content)-1; i += 2 { 794 var err error 795 if m[node.Content[i].Value], err = subSpec.YAMLToValue(node.Content[i+1], conf); err != nil { 796 return nil, err 797 } 798 } 799 return m, nil 800 } 801 switch f.Type { 802 case FieldTypeString: 803 var s string 804 if err := node.Decode(&s); err != nil { 805 return nil, err 806 } 807 return s, nil 808 case FieldTypeInt: 809 var i int 810 if err := node.Decode(&i); err != nil { 811 return nil, err 812 } 813 return i, nil 814 case FieldTypeFloat: 815 var f float64 816 if err := node.Decode(&f); err != nil { 817 return nil, err 818 } 819 return f, nil 820 case FieldTypeBool: 821 var b bool 822 if err := node.Decode(&b); err != nil { 823 return nil, err 824 } 825 return b, nil 826 case FieldTypeObject: 827 return f.Children.YAMLToMap(node, conf) 828 } 829 830 if conf.FallbackToInterface { 831 // We don't know what the field actually is (likely a component 832 // type), so if we we can either decode into a generic interface 833 // or return the raw node itself. 834 var v interface{} 835 if err := node.Decode(&v); err != nil { 836 return nil, err 837 } 838 return v, nil 839 } 840 return node, nil 841 } 842 843 // YAMLToMap converts a yaml node into a generic map structure by referencing 844 // expected fields, adding default values to the map when the node does not 845 // contain them. 846 func (f FieldSpecs) YAMLToMap(node *yaml.Node, conf ToValueConfig) (map[string]interface{}, error) { 847 node = unwrapDocumentNode(node) 848 849 pendingFieldsMap := map[string]FieldSpec{} 850 for _, field := range f { 851 pendingFieldsMap[field.Name] = field 852 } 853 854 resultMap := map[string]interface{}{} 855 856 for i := 0; i < len(node.Content)-1; i += 2 { 857 fieldName := node.Content[i].Value 858 859 if f, exists := pendingFieldsMap[fieldName]; exists { 860 delete(pendingFieldsMap, f.Name) 861 var err error 862 if resultMap[fieldName], err = f.YAMLToValue(node.Content[i+1], conf); err != nil { 863 return nil, fmt.Errorf("field '%v': %w", fieldName, err) 864 } 865 } else { 866 var v interface{} 867 if err := node.Content[i+1].Decode(&v); err != nil { 868 return nil, err 869 } 870 resultMap[fieldName] = v 871 } 872 } 873 874 for k, v := range pendingFieldsMap { 875 defValue, err := getDefault(k, v) 876 if err != nil { 877 if v.needsDefault() && !conf.Passive { 878 return nil, err 879 } 880 continue 881 } 882 resultMap[k] = defValue 883 } 884 885 return resultMap, nil 886 } 887 888 //------------------------------------------------------------------------------ 889 890 func unwrapDocumentNode(node *yaml.Node) *yaml.Node { 891 if node != nil && node.Kind == yaml.DocumentNode && len(node.Content) > 0 { 892 return node.Content[0] 893 } 894 return node 895 }