Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .chloggen/mdatagen-array-validation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. receiver/otlp)
component: cmd/mdatagen

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Handle array validators in generated config structs

# One or more tracking issues or pull requests related to the change
issues: [14808]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext: Supported validators include `minItems`, `maxItems`, `uniqueItems` and `contains`.

# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
change_logs: [api]
37 changes: 35 additions & 2 deletions cmd/mdatagen/internal/cfggen/generation.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,10 @@ func collectImports(md *ConfigMetadata, imports map[string]bool, rootPackage, co
imports["slices"] = true
}

if md.Contains != nil && len(md.Contains.Enum) > 0 {
imports["slices"] = true
}

for _, prop := range md.Properties {
if err := collectImports(prop, imports, rootPackage, componentPackage); err != nil {
return err
Expand Down Expand Up @@ -400,7 +404,9 @@ func hasValidators(md *ConfigMetadata) bool {
return md.GoStruct.CustomValidator != nil || // custom validation
len(md.Required) > 0 || // required validation
md.MinLength != nil || md.MaxLength != nil || md.Pattern != "" || // string validation
md.Minimum != nil || md.Maximum != nil || md.ExclusiveMinimum != nil || md.ExclusiveMaximum != nil // numeric validation
md.Minimum != nil || md.Maximum != nil || md.ExclusiveMinimum != nil || md.ExclusiveMaximum != nil || // numeric validation
md.MinItems != nil || md.MaxItems != nil || md.UniqueItems || // array validation
(md.Contains != nil && len(md.Contains.Enum) > 0) // contains validation
}

// FormatTypeName resolves a reference string to a Go type expression using GoTypeRef.
Expand Down Expand Up @@ -507,12 +513,19 @@ type ValidationRules struct {
ExclusiveMinimum *float64
ExclusiveMaximum *float64
Enum []any
MinItems *int
MaxItems *int
UniqueItems bool
ItemGoType string
ContainsEnum []any
ItemFieldType string
}

func (vr *ValidationRules) HasValueRule() bool {
return vr.MaxLength != nil || vr.MinLength != nil || vr.Pattern != nil ||
vr.Minimum != nil || vr.Maximum != nil || vr.ExclusiveMinimum != nil || vr.ExclusiveMaximum != nil ||
len(vr.Enum) > 0
len(vr.Enum) > 0 ||
vr.MinItems != nil || vr.MaxItems != nil || vr.UniqueItems || len(vr.ContainsEnum) > 0
}

func (vr *ValidationRules) Enabled() bool {
Expand Down Expand Up @@ -542,6 +555,19 @@ func collectValidators(md *ConfigMetadata, validators *[]Validator) {
ExclusiveMinimum: prop.ExclusiveMinimum,
ExclusiveMaximum: prop.ExclusiveMaximum,
Enum: prop.Enum,
MinItems: prop.MinItems,
MaxItems: prop.MaxItems,
UniqueItems: prop.UniqueItems,
}

if prop.UniqueItems && prop.Values != nil {
rules.ItemGoType = schemaTypeToGoType(prop.Values.Type)
}
if prop.Contains != nil && len(prop.Contains.Enum) > 0 {
rules.ContainsEnum = prop.Contains.Enum
if prop.Values != nil {
rules.ItemFieldType = string(prop.Values.Type)
}
}

rules.Required = slices.Contains(md.Required, propName)
Expand Down Expand Up @@ -597,6 +623,13 @@ func resolveType(md *ConfigMetadata) string {
}
}

func schemaTypeToGoType(schemaType SchemaType) string {
if goType, ok := primitiveSchemaGoTypes[schemaType]; ok {
return goType
}
return "any"
}

func generateValidatorName(propName string, desc *CustomValidatorConfig) string {
if desc.Name != "" {
return desc.Name
Expand Down
247 changes: 247 additions & 0 deletions cmd/mdatagen/internal/cfggen/generation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3098,3 +3098,250 @@ func TestInvalidTestValue(t *testing.T) {
require.Equal(t, "-1.0", invalidTestValue("number"))
require.Equal(t, `"__invalid__"`, invalidTestValue("object"))
}

func TestExtractValidators_ArrayValidators(t *testing.T) {
minItems := 1
maxItems := 10

tests := []struct {
name string
metadata *ConfigMetadata
expected []Validator
}{
{
name: "minItems only",
metadata: &ConfigMetadata{
Type: ObjectType,
Properties: map[string]*ConfigMetadata{
"tags": {Type: SliceType, Values: &ConfigMetadata{Type: StringType}, MinItems: &minItems},
},
},
expected: []Validator{
{
FieldName: "tags",
FieldType: "slice",
Rules: ValidationRules{MinItems: &minItems},
},
},
},
{
name: "maxItems only",
metadata: &ConfigMetadata{
Type: ObjectType,
Properties: map[string]*ConfigMetadata{
"tags": {Type: SliceType, Values: &ConfigMetadata{Type: StringType}, MaxItems: &maxItems},
},
},
expected: []Validator{
{
FieldName: "tags",
FieldType: "slice",
Rules: ValidationRules{MaxItems: &maxItems},
},
},
},
{
name: "uniqueItems only",
metadata: &ConfigMetadata{
Type: ObjectType,
Properties: map[string]*ConfigMetadata{
"tags": {Type: SliceType, Values: &ConfigMetadata{Type: StringType}, UniqueItems: true},
},
},
expected: []Validator{
{
FieldName: "tags",
FieldType: "slice",
Rules: ValidationRules{UniqueItems: true, ItemGoType: "string"},
},
},
},
{
name: "uniqueItems with integer items",
metadata: &ConfigMetadata{
Type: ObjectType,
Properties: map[string]*ConfigMetadata{
"ids": {Type: SliceType, Values: &ConfigMetadata{Type: IntType}, UniqueItems: true},
},
},
expected: []Validator{
{
FieldName: "ids",
FieldType: "slice",
Rules: ValidationRules{UniqueItems: true, ItemGoType: "int"},
},
},
},
{
name: "uniqueItems without values schema falls back to any",
metadata: &ConfigMetadata{
Type: ObjectType,
Properties: map[string]*ConfigMetadata{
"tags": {Type: SliceType, UniqueItems: true},
},
},
expected: []Validator{
{
FieldName: "tags",
FieldType: "slice",
Rules: ValidationRules{UniqueItems: true},
},
},
},
{
name: "contains with enum",
metadata: &ConfigMetadata{
Type: ObjectType,
Properties: map[string]*ConfigMetadata{
"tags": {
Type: SliceType,
Values: &ConfigMetadata{Type: StringType},
Contains: &ConfigMetadata{Type: StringType, Enum: []any{"production", "staging"}},
},
},
},
expected: []Validator{
{
FieldName: "tags",
FieldType: "slice",
Rules: ValidationRules{ContainsEnum: []any{"production", "staging"}, ItemFieldType: "string"},
},
},
},
{
name: "all array validators combined",
metadata: &ConfigMetadata{
Type: ObjectType,
Properties: map[string]*ConfigMetadata{
"tags": {
Type: SliceType,
Values: &ConfigMetadata{Type: StringType},
MinItems: &minItems,
MaxItems: &maxItems,
UniqueItems: true,
Contains: &ConfigMetadata{Type: StringType, Enum: []any{"production"}},
},
},
},
expected: []Validator{
{
FieldName: "tags",
FieldType: "slice",
Rules: ValidationRules{
MinItems: &minItems,
MaxItems: &maxItems,
UniqueItems: true,
ItemGoType: "string",
ContainsEnum: []any{"production"},
ItemFieldType: "string",
},
},
},
},
{
name: "no array validators produces no validator",
metadata: &ConfigMetadata{
Type: ObjectType,
Properties: map[string]*ConfigMetadata{
"tags": {Type: SliceType, Values: &ConfigMetadata{Type: StringType}},
},
},
expected: []Validator{},
},
{
name: "required combined with array validators",
metadata: &ConfigMetadata{
Type: ObjectType,
Required: []string{"tags"},
Properties: map[string]*ConfigMetadata{
"tags": {Type: SliceType, Values: &ConfigMetadata{Type: StringType}, MinItems: &minItems},
},
},
expected: []Validator{
{
FieldName: "tags",
FieldType: "slice",
Rules: ValidationRules{Required: true, MinItems: &minItems},
},
},
},
{
name: "array validators on pointer array",
metadata: &ConfigMetadata{
Type: ObjectType,
Properties: map[string]*ConfigMetadata{
"targets": {Type: SliceType, Values: &ConfigMetadata{Type: StringType}, IsPointer: true, MinItems: &minItems, MaxItems: &maxItems},
},
},
expected: []Validator{
{
FieldName: "targets",
FieldType: "slice",
IsPointer: true,
Rules: ValidationRules{MinItems: &minItems, MaxItems: &maxItems},
},
},
},
{
name: "contains without enum produces no containsEnum rule",
metadata: &ConfigMetadata{
Type: ObjectType,
Properties: map[string]*ConfigMetadata{
"tags": {
Type: SliceType,
Values: &ConfigMetadata{Type: StringType},
Contains: &ConfigMetadata{Type: StringType},
},
},
},
expected: []Validator{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ExtractValidators(tt.metadata)
require.Equal(t, tt.expected, result)
})
}
}

func TestValidationRules_HasValueRule_ArrayValidators(t *testing.T) {
tests := []struct {
name string
rules ValidationRules
expected bool
}{
{
name: "minItems",
rules: ValidationRules{MinItems: Ptr(1)},
expected: true,
},
{
name: "maxItems",
rules: ValidationRules{MaxItems: Ptr(10)},
expected: true,
},
{
name: "uniqueItems",
rules: ValidationRules{UniqueItems: true},
expected: true,
},
{
name: "containsEnum",
rules: ValidationRules{ContainsEnum: []any{"a"}},
expected: true,
},
{
name: "all array validators",
rules: ValidationRules{MinItems: Ptr(1), MaxItems: Ptr(10), UniqueItems: true, ContainsEnum: []any{"a"}},
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
require.Equal(t, tt.expected, tt.rules.HasValueRule())
})
}
}
1 change: 1 addition & 0 deletions cmd/mdatagen/internal/samplescraper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ This scraper is used for testing purposes to check the output of mdatagen.
| `log_level` | string (one of: debug, info, warn, error) | info | no | Logging level for the scraper. |
| `metrics` | object (see [metrics](#metrics)) | | no | MetricsConfig provides config for sample metrics. |
| `resource_attributes` | object (see [resource_attributes](#resource_attributes)) | | no | ResourceAttributesConfig provides config for sample resource attributes. |
| `tags` | []string | [production] | no | Tags for the scraper instance. |
| `targets` | []object | [{}] | **yes** | List of targets to scrape metrics from. |
| `timeout` | duration | 0 | no | An optional value used to set scraper's context deadline. |

Expand Down
22 changes: 21 additions & 1 deletion cmd/mdatagen/internal/samplescraper/config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@
"error"
]
},
"tags": {
"type": "array",
"items": {
"type": "string"
},
"description": "Tags for the scraper instance.",
"default": [
"production"
],
"uniqueItems": true,
"contains": {
"type": "string",
"enum": [
"production",
"staging"
]
}
},
"targets": {
"type": "array",
"items": {
Expand Down Expand Up @@ -67,7 +85,9 @@
"description": "List of targets to scrape metrics from.",
"default": [
{}
]
],
"minItems": 1,
"maxItems": 100
}
},
"$id": "go.opentelemetry.io/collector/cmd/mdatagen/internal/samplescraper",
Expand Down
Loading