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  }