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 }