github.com/Jeffail/benthos/v3@v3.65.0/internal/docs/field.go (about)

     1  package docs
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"github.com/Jeffail/benthos/v3/internal/bloblang"
     7  )
     8  
     9  // FieldType represents a field type.
    10  type FieldType string
    11  
    12  // ValueType variants.
    13  var (
    14  	FieldTypeString  FieldType = "string"
    15  	FieldTypeInt     FieldType = "int"
    16  	FieldTypeFloat   FieldType = "float"
    17  	FieldTypeBool    FieldType = "bool"
    18  	FieldTypeObject  FieldType = "object"
    19  	FieldTypeUnknown FieldType = "unknown"
    20  
    21  	// Core component types, only components that can be a child of another
    22  	// component config are listed here.
    23  	FieldTypeInput     FieldType = "input"
    24  	FieldTypeBuffer    FieldType = "buffer"
    25  	FieldTypeCache     FieldType = "cache"
    26  	FieldTypeCondition FieldType = "condition"
    27  	FieldTypeProcessor FieldType = "processor"
    28  	FieldTypeRateLimit FieldType = "rate_limit"
    29  	FieldTypeOutput    FieldType = "output"
    30  	FieldTypeMetrics   FieldType = "metrics"
    31  	FieldTypeTracer    FieldType = "tracer"
    32  )
    33  
    34  // IsCoreComponent returns the core component type of a field if applicable.
    35  func (t FieldType) IsCoreComponent() (Type, bool) {
    36  	switch t {
    37  	case FieldTypeInput:
    38  		return TypeInput, true
    39  	case FieldTypeBuffer:
    40  		return TypeBuffer, true
    41  	case FieldTypeCache:
    42  		return TypeCache, true
    43  	case FieldTypeCondition:
    44  		// TODO: V4 Remove this
    45  		return "condition", true
    46  	case FieldTypeProcessor:
    47  		return TypeProcessor, true
    48  	case FieldTypeRateLimit:
    49  		return TypeRateLimit, true
    50  	case FieldTypeOutput:
    51  		return TypeOutput, true
    52  	case FieldTypeTracer:
    53  		return TypeTracer, true
    54  	case FieldTypeMetrics:
    55  		return TypeMetrics, true
    56  	}
    57  	return "", false
    58  }
    59  
    60  // FieldKind represents a field kind.
    61  type FieldKind string
    62  
    63  // ValueType variants.
    64  var (
    65  	KindScalar  FieldKind = "scalar"
    66  	KindArray   FieldKind = "array"
    67  	Kind2DArray FieldKind = "2darray"
    68  	KindMap     FieldKind = "map"
    69  )
    70  
    71  //------------------------------------------------------------------------------
    72  
    73  // FieldSpec describes a component config field.
    74  type FieldSpec struct {
    75  	// Name of the field (as it appears in config).
    76  	Name string `json:"name"`
    77  
    78  	// Type of the field.
    79  	//
    80  	// TODO: Make this mandatory
    81  	Type FieldType `json:"type"`
    82  
    83  	// Kind of the field.
    84  	Kind FieldKind `json:"kind"`
    85  
    86  	// Description of the field purpose (in markdown).
    87  	Description string `json:"description,omitempty"`
    88  
    89  	// IsAdvanced is true for optional fields that will not be present in most
    90  	// configs.
    91  	IsAdvanced bool `json:"is_advanced,omitempty"`
    92  
    93  	// IsDeprecated is true for fields that are deprecated and only exist
    94  	// for backwards compatibility reasons.
    95  	IsDeprecated bool `json:"is_deprecated,omitempty"`
    96  
    97  	// IsOptional is a boolean flag indicating that a field is optional, even
    98  	// if there is no default. This prevents linting errors when the field
    99  	// is missing.
   100  	IsOptional bool `json:"is_optional,omitempty"`
   101  
   102  	// Default value of the field.
   103  	Default *interface{} `json:"default,omitempty"`
   104  
   105  	// Interpolation indicates that the field supports interpolation
   106  	// functions.
   107  	Interpolated bool `json:"interpolated,omitempty"`
   108  
   109  	// Bloblang indicates that a string field is a Bloblang mapping.
   110  	Bloblang bool `json:"bloblang,omitempty"`
   111  
   112  	// Examples is a slice of optional example values for a field.
   113  	Examples []interface{} `json:"examples,omitempty"`
   114  
   115  	// AnnotatedOptions for this field. Each option should have a summary.
   116  	AnnotatedOptions [][2]string `json:"annotated_options,omitempty"`
   117  
   118  	// Options for this field.
   119  	Options []string `json:"options,omitempty"`
   120  
   121  	// Children fields of this field (it must be an object).
   122  	Children FieldSpecs `json:"children,omitempty"`
   123  
   124  	// Version is an explicit version when this field was introduced.
   125  	Version string `json:"version,omitempty"`
   126  
   127  	omitWhenFn   func(field, parent interface{}) (why string, shouldOmit bool)
   128  	customLintFn LintFunc
   129  	skipLint     bool
   130  }
   131  
   132  // IsInterpolated indicates that the field supports interpolation functions.
   133  func (f FieldSpec) IsInterpolated() FieldSpec {
   134  	f.Interpolated = true
   135  	f.customLintFn = LintBloblangField
   136  	return f
   137  }
   138  
   139  // IsBloblang indicates that the field is a Bloblang mapping.
   140  func (f FieldSpec) IsBloblang() FieldSpec {
   141  	f.Bloblang = true
   142  	f.customLintFn = LintBloblangMapping
   143  	return f
   144  }
   145  
   146  // HasType returns a new FieldSpec that specifies a specific type.
   147  func (f FieldSpec) HasType(t FieldType) FieldSpec {
   148  	f.Type = t
   149  	return f
   150  }
   151  
   152  // Optional marks this field as being optional, and therefore its absence in a
   153  // config is not considered an error even when a default value is not provided.
   154  func (f FieldSpec) Optional() FieldSpec {
   155  	f.IsOptional = true
   156  	return f
   157  }
   158  
   159  // Advanced marks this field as being advanced, and therefore not commonly used.
   160  func (f FieldSpec) Advanced() FieldSpec {
   161  	f.IsAdvanced = true
   162  	for i, v := range f.Children {
   163  		f.Children[i] = v.Advanced()
   164  	}
   165  	return f
   166  }
   167  
   168  // Array determines that this field is an array of the field type.
   169  func (f FieldSpec) Array() FieldSpec {
   170  	f.Kind = KindArray
   171  	return f
   172  }
   173  
   174  // ArrayOfArrays determines that this is an array of arrays of the field type.
   175  func (f FieldSpec) ArrayOfArrays() FieldSpec {
   176  	f.Kind = Kind2DArray
   177  	return f
   178  }
   179  
   180  // Map determines that this field is a map of arbitrary keys to a field type.
   181  func (f FieldSpec) Map() FieldSpec {
   182  	f.Kind = KindMap
   183  	return f
   184  }
   185  
   186  // Scalar determines that this field is a scalar type (the default).
   187  func (f FieldSpec) Scalar() FieldSpec {
   188  	f.Kind = KindScalar
   189  	return f
   190  }
   191  
   192  // HasDefault returns a new FieldSpec that specifies a default value.
   193  func (f FieldSpec) HasDefault(v interface{}) FieldSpec {
   194  	f.Default = &v
   195  	return f
   196  }
   197  
   198  // AtVersion specifies the version at which this fields behaviour was last
   199  // modified.
   200  func (f FieldSpec) AtVersion(v string) FieldSpec {
   201  	f.Version = v
   202  	return f
   203  }
   204  
   205  // HasAnnotatedOptions returns a new FieldSpec that specifies a specific list of
   206  // annotated options. Either
   207  func (f FieldSpec) HasAnnotatedOptions(options ...string) FieldSpec {
   208  	if len(f.Options) > 0 {
   209  		panic("cannot combine annotated and non-annotated options for a field")
   210  	}
   211  	if len(options)%2 != 0 {
   212  		panic("annotated field options must each have a summary")
   213  	}
   214  	for i := 0; i < len(options); i += 2 {
   215  		f.AnnotatedOptions = append(f.AnnotatedOptions, [2]string{
   216  			options[i], options[i+1],
   217  		})
   218  	}
   219  	return f
   220  }
   221  
   222  // HasOptions returns a new FieldSpec that specifies a specific list of options.
   223  func (f FieldSpec) HasOptions(options ...string) FieldSpec {
   224  	if len(f.AnnotatedOptions) > 0 {
   225  		panic("cannot combine annotated and non-annotated options for a field")
   226  	}
   227  	f.Options = options
   228  	return f
   229  }
   230  
   231  // WithChildren returns a new FieldSpec that has child fields.
   232  func (f FieldSpec) WithChildren(children ...FieldSpec) FieldSpec {
   233  	if len(f.Type) == 0 {
   234  		f.Type = FieldTypeObject
   235  	}
   236  	if f.IsAdvanced {
   237  		for i, v := range children {
   238  			children[i] = v.Advanced()
   239  		}
   240  	}
   241  	f.Children = append(f.Children, children...)
   242  	return f
   243  }
   244  
   245  // OmitWhen specifies a custom func that, when provided a generic config struct,
   246  // returns a boolean indicating when the field can be safely omitted from a
   247  // config.
   248  func (f FieldSpec) OmitWhen(fn func(field, parent interface{}) (why string, shouldOmit bool)) FieldSpec {
   249  	f.omitWhenFn = fn
   250  	return f
   251  }
   252  
   253  // Linter adds a linting function to a field. When linting is performed on a
   254  // config the provided function will be called with a boxed variant of the field
   255  // value, allowing it to perform linting on that value.
   256  func (f FieldSpec) Linter(fn LintFunc) FieldSpec {
   257  	f.customLintFn = fn
   258  	return f
   259  }
   260  
   261  // LintOptions enforces that a field value matches one of the provided options
   262  // and returns a linting error if that is not the case. This is currently opt-in
   263  // because some fields express options that are only a subset due to deprecated
   264  // functionality.
   265  //
   266  // TODO: V4 Switch this to opt-out.
   267  func (f FieldSpec) LintOptions() FieldSpec {
   268  	f.customLintFn = func(ctx LintContext, line, col int, value interface{}) []Lint {
   269  		str, ok := value.(string)
   270  		if !ok {
   271  			return nil
   272  		}
   273  		if len(f.Options) > 0 {
   274  			for _, optStr := range f.Options {
   275  				if str == optStr {
   276  					return nil
   277  				}
   278  			}
   279  		} else {
   280  			for _, optStr := range f.AnnotatedOptions {
   281  				if str == optStr[0] {
   282  					return nil
   283  				}
   284  			}
   285  		}
   286  		return []Lint{NewLintError(line, fmt.Sprintf("value %v is not a valid option for this field", str))}
   287  	}
   288  	return f
   289  }
   290  
   291  // Unlinted returns a field spec that will not be lint checked during a config
   292  // parse.
   293  func (f FieldSpec) Unlinted() FieldSpec {
   294  	f.skipLint = true
   295  	return f
   296  }
   297  
   298  // GetLintFunc returns a lint func for the field if one is applicable, otherwise
   299  // nil is returned.
   300  func (f FieldSpec) GetLintFunc() LintFunc {
   301  	if f.customLintFn != nil {
   302  		return f.customLintFn
   303  	}
   304  	if f.Interpolated {
   305  		return LintBloblangField
   306  	}
   307  	if f.Bloblang {
   308  		return LintBloblangMapping
   309  	}
   310  	return nil
   311  }
   312  
   313  func (f FieldSpec) shouldOmit(field, parent interface{}) (string, bool) {
   314  	if f.omitWhenFn == nil {
   315  		return "", false
   316  	}
   317  	return f.omitWhenFn(field, parent)
   318  }
   319  
   320  // FieldObject returns a field spec for an object typed field.
   321  func FieldObject(name, description string, examples ...interface{}) FieldSpec {
   322  	return FieldCommon(name, description, examples...).HasType(FieldTypeObject)
   323  }
   324  
   325  // FieldString returns a field spec for a common string typed field.
   326  func FieldString(name, description string, examples ...interface{}) FieldSpec {
   327  	return FieldCommon(name, description, examples...).HasType(FieldTypeString)
   328  }
   329  
   330  // FieldInterpolatedString returns a field spec for a string typed field
   331  // supporting dynamic interpolated functions.
   332  func FieldInterpolatedString(name, description string, examples ...interface{}) FieldSpec {
   333  	return FieldCommon(name, description, examples...).HasType(FieldTypeString).IsInterpolated()
   334  }
   335  
   336  // FieldBloblang returns a field spec for a string typed field containing a
   337  // Bloblang mapping.
   338  func FieldBloblang(name, description string, examples ...interface{}) FieldSpec {
   339  	return FieldCommon(name, description, examples...).HasType(FieldTypeString).IsBloblang()
   340  }
   341  
   342  // FieldInt returns a field spec for a common int typed field.
   343  func FieldInt(name, description string, examples ...interface{}) FieldSpec {
   344  	return FieldCommon(name, description, examples...).HasType(FieldTypeInt)
   345  }
   346  
   347  // FieldFloat returns a field spec for a common float typed field.
   348  func FieldFloat(name, description string, examples ...interface{}) FieldSpec {
   349  	return FieldCommon(name, description, examples...).HasType(FieldTypeFloat)
   350  }
   351  
   352  // FieldBool returns a field spec for a common bool typed field.
   353  func FieldBool(name, description string, examples ...interface{}) FieldSpec {
   354  	return FieldCommon(name, description, examples...).HasType(FieldTypeBool)
   355  }
   356  
   357  // FieldAdvanced returns a field spec for an advanced field.
   358  func FieldAdvanced(name, description string, examples ...interface{}) FieldSpec {
   359  	return FieldSpec{
   360  		Name:        name,
   361  		Description: description,
   362  		Kind:        KindScalar,
   363  		IsAdvanced:  true,
   364  		Examples:    examples,
   365  	}
   366  }
   367  
   368  // FieldCommon returns a field spec for a common field.
   369  func FieldCommon(name, description string, examples ...interface{}) FieldSpec {
   370  	return FieldSpec{
   371  		Name:        name,
   372  		Description: description,
   373  		Kind:        KindScalar,
   374  		Examples:    examples,
   375  	}
   376  }
   377  
   378  // FieldComponent returns a field spec for a component.
   379  func FieldComponent() FieldSpec {
   380  	return FieldSpec{
   381  		Kind: KindScalar,
   382  	}
   383  }
   384  
   385  // FieldDeprecated returns a field spec for a deprecated field.
   386  func FieldDeprecated(name string, description ...string) FieldSpec {
   387  	desc := "DEPRECATED: Do not use."
   388  	if len(description) > 0 {
   389  		desc = "DEPRECATED: " + description[0]
   390  	}
   391  	return FieldSpec{
   392  		Name:         name,
   393  		Description:  desc,
   394  		Kind:         KindScalar,
   395  		IsDeprecated: true,
   396  	}
   397  }
   398  
   399  func (f FieldSpec) sanitise(s interface{}, filter FieldFilter) {
   400  	if coreType, isCore := f.Type.IsCoreComponent(); isCore {
   401  		switch f.Kind {
   402  		case KindArray:
   403  			if arr, ok := s.([]interface{}); ok {
   404  				for _, ele := range arr {
   405  					_ = SanitiseComponentConfig(coreType, ele, filter)
   406  				}
   407  			}
   408  		case KindMap:
   409  			if obj, ok := s.(map[string]interface{}); ok {
   410  				for _, v := range obj {
   411  					_ = SanitiseComponentConfig(coreType, v, filter)
   412  				}
   413  			}
   414  		default:
   415  			_ = SanitiseComponentConfig(coreType, s, filter)
   416  		}
   417  	} else if len(f.Children) > 0 {
   418  		switch f.Kind {
   419  		case KindArray:
   420  			if arr, ok := s.([]interface{}); ok {
   421  				for _, ele := range arr {
   422  					f.Children.sanitise(ele, filter)
   423  				}
   424  			}
   425  		case KindMap:
   426  			if obj, ok := s.(map[string]interface{}); ok {
   427  				for _, v := range obj {
   428  					f.Children.sanitise(v, filter)
   429  				}
   430  			}
   431  		default:
   432  			f.Children.sanitise(s, filter)
   433  		}
   434  	}
   435  }
   436  
   437  //------------------------------------------------------------------------------
   438  
   439  // FieldSpecs is a slice of field specs for a component.
   440  type FieldSpecs []FieldSpec
   441  
   442  // Merge with another set of FieldSpecs.
   443  func (f FieldSpecs) Merge(specs FieldSpecs) FieldSpecs {
   444  	return append(f, specs...)
   445  }
   446  
   447  // Add more field specs.
   448  func (f FieldSpecs) Add(specs ...FieldSpec) FieldSpecs {
   449  	return append(f, specs...)
   450  }
   451  
   452  // FieldFilter defines a filter closure that returns a boolean for a component
   453  // field indicating whether the field should be kept within a generated config.
   454  type FieldFilter func(spec FieldSpec) bool
   455  
   456  func (f FieldFilter) shouldDrop(spec FieldSpec) bool {
   457  	if f == nil {
   458  		return false
   459  	}
   460  	return !f(spec)
   461  }
   462  
   463  // ShouldDropDeprecated returns a field filter that removes all deprecated
   464  // fields when the boolean argument is true.
   465  func ShouldDropDeprecated(b bool) FieldFilter {
   466  	if !b {
   467  		return nil
   468  	}
   469  	return func(spec FieldSpec) bool {
   470  		return !spec.IsDeprecated
   471  	}
   472  }
   473  
   474  func (f FieldSpecs) sanitise(s interface{}, filter FieldFilter) {
   475  	m, ok := s.(map[string]interface{})
   476  	if !ok {
   477  		return
   478  	}
   479  	for _, spec := range f {
   480  		if filter.shouldDrop(spec) {
   481  			delete(m, spec.Name)
   482  			continue
   483  		}
   484  		v := m[spec.Name]
   485  		if _, omit := spec.shouldOmit(v, m); omit {
   486  			delete(m, spec.Name)
   487  		} else {
   488  			spec.sanitise(v, filter)
   489  		}
   490  	}
   491  }
   492  
   493  //------------------------------------------------------------------------------
   494  
   495  // LintContext is provided to linting functions, and provides context about the
   496  // wider configuration.
   497  type LintContext struct {
   498  	// A map of label names to the line they were defined at.
   499  	LabelsToLine map[string]int
   500  
   501  	// Provides documentation for component implementations.
   502  	DocsProvider Provider
   503  
   504  	// Provides an isolated context for Bloblang parsing.
   505  	BloblangEnv *bloblang.Environment
   506  
   507  	// Config fields
   508  
   509  	// Reject any deprecated components or fields as linting errors.
   510  	RejectDeprecated bool
   511  }
   512  
   513  // NewLintContext creates a new linting context.
   514  func NewLintContext() LintContext {
   515  	return LintContext{
   516  		LabelsToLine:     map[string]int{},
   517  		DocsProvider:     globalProvider,
   518  		BloblangEnv:      bloblang.GlobalEnvironment().Deactivated(),
   519  		RejectDeprecated: false,
   520  	}
   521  }
   522  
   523  // LintFunc is a common linting function for field values.
   524  type LintFunc func(ctx LintContext, line, col int, value interface{}) []Lint
   525  
   526  // LintLevel describes the severity level of a linting error.
   527  type LintLevel int
   528  
   529  // Lint levels
   530  const (
   531  	LintError   LintLevel = iota
   532  	LintWarning LintLevel = iota
   533  )
   534  
   535  // Lint describes a single linting issue found with a Benthos config.
   536  type Lint struct {
   537  	Line   int
   538  	Column int // Optional, omitted from lint report unless >= 1
   539  	Level  LintLevel
   540  	What   string
   541  }
   542  
   543  // NewLintError returns an error lint.
   544  func NewLintError(line int, msg string) Lint {
   545  	return Lint{Line: line, Level: LintError, What: msg}
   546  }
   547  
   548  // NewLintWarning returns a warning lint.
   549  func NewLintWarning(line int, msg string) Lint {
   550  	return Lint{Line: line, Level: LintWarning, What: msg}
   551  }
   552  
   553  //------------------------------------------------------------------------------
   554  
   555  func (f FieldSpec) needsDefault() bool {
   556  	if f.IsOptional {
   557  		return false
   558  	}
   559  	if f.IsDeprecated {
   560  		return false
   561  	}
   562  	return true
   563  }
   564  
   565  func getDefault(pathName string, field FieldSpec) (interface{}, error) {
   566  	if field.Default != nil {
   567  		// TODO: Should be deep copy here?
   568  		return *field.Default, nil
   569  	} else if field.Kind == KindArray {
   570  		return []interface{}{}, nil
   571  	} else if field.Kind == Kind2DArray {
   572  		return []interface{}{}, nil
   573  	} else if field.Kind == KindMap {
   574  		return map[string]interface{}{}, nil
   575  	} else if len(field.Children) > 0 {
   576  		m := map[string]interface{}{}
   577  		for _, v := range field.Children {
   578  			defV, err := getDefault(pathName+"."+v.Name, v)
   579  			if err == nil {
   580  				m[v.Name] = defV
   581  			} else if v.needsDefault() {
   582  				return nil, err
   583  			}
   584  		}
   585  		return m, nil
   586  	}
   587  	return nil, fmt.Errorf("field '%v' is required and was not present in the config", pathName)
   588  }