Skip to content

Commit 3fa45c5

Browse files
committed
cmd/mdatagen: handle array validators in generated config structs
Signed-off-by: Mujib Ahasan <ahasanmujib8@gmail.com>
1 parent 52e6bf4 commit 3fa45c5

11 files changed

Lines changed: 565 additions & 10 deletions

File tree

cmd/mdatagen/internal/cfggen/generation.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,10 @@ func collectImports(md *ConfigMetadata, imports map[string]bool, rootPackage, co
349349
imports["slices"] = true
350350
}
351351

352+
if md.Contains != nil && len(md.Contains.Enum) > 0 {
353+
imports["slices"] = true
354+
}
355+
352356
for _, prop := range md.Properties {
353357
if err := collectImports(prop, imports, rootPackage, componentPackage); err != nil {
354358
return err
@@ -408,7 +412,9 @@ func hasValidators(md *ConfigMetadata) bool {
408412
return md.GoStruct.CustomValidator != nil || // custom validation
409413
len(md.Required) > 0 || // required validation
410414
md.MinLength != nil || md.MaxLength != nil || md.Pattern != "" || // string validation
411-
md.Minimum != nil || md.Maximum != nil || md.ExclusiveMinimum != nil || md.ExclusiveMaximum != nil // numeric validation
415+
md.Minimum != nil || md.Maximum != nil || md.ExclusiveMinimum != nil || md.ExclusiveMaximum != nil || // numeric validation
416+
md.MinItems != nil || md.MaxItems != nil || md.UniqueItems || // array validation
417+
(md.Contains != nil && len(md.Contains.Enum) > 0) // contains validation
412418
}
413419

414420
// FormatTypeName resolves a reference string to a Go type expression using GoTypeRef.
@@ -519,12 +525,19 @@ type ValidationRules struct {
519525
ExclusiveMinimum *float64
520526
ExclusiveMaximum *float64
521527
Enum []any
528+
MinItems *int
529+
MaxItems *int
530+
UniqueItems bool
531+
ItemGoType string
532+
ContainsEnum []any
533+
ItemFieldType string
522534
}
523535

524536
func (vr *ValidationRules) HasValueRule() bool {
525537
return vr.MaxLength != nil || vr.MinLength != nil || vr.Pattern != nil ||
526538
vr.Minimum != nil || vr.Maximum != nil || vr.ExclusiveMinimum != nil || vr.ExclusiveMaximum != nil ||
527-
len(vr.Enum) > 0
539+
len(vr.Enum) > 0 ||
540+
vr.MinItems != nil || vr.MaxItems != nil || vr.UniqueItems || len(vr.ContainsEnum) > 0
528541
}
529542

530543
func (vr *ValidationRules) Enabled() bool {
@@ -554,6 +567,19 @@ func collectValidators(md *ConfigMetadata, validators *[]Validator) {
554567
ExclusiveMinimum: prop.ExclusiveMinimum,
555568
ExclusiveMaximum: prop.ExclusiveMaximum,
556569
Enum: prop.Enum,
570+
MinItems: prop.MinItems,
571+
MaxItems: prop.MaxItems,
572+
UniqueItems: prop.UniqueItems,
573+
}
574+
575+
if prop.UniqueItems && prop.Items != nil {
576+
rules.ItemGoType = schemaTypeToGoType(prop.Items.Type)
577+
}
578+
if prop.Contains != nil && len(prop.Contains.Enum) > 0 {
579+
rules.ContainsEnum = prop.Contains.Enum
580+
if prop.Items != nil {
581+
rules.ItemFieldType = prop.Items.Type
582+
}
557583
}
558584

559585
rules.Required = slices.Contains(md.Required, propName)
@@ -611,6 +637,13 @@ func resolveType(md *ConfigMetadata) string {
611637
}
612638
}
613639

640+
func schemaTypeToGoType(schemaType string) string {
641+
if goType, ok := primitiveSchemaGoTypes[schemaType]; ok {
642+
return goType
643+
}
644+
return "any"
645+
}
646+
614647
func generateValidatorName(propName string, desc *CustomValidatorConfig) string {
615648
if desc.Name != "" {
616649
return desc.Name

cmd/mdatagen/internal/cfggen/generation_test.go

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3099,3 +3099,250 @@ func TestInvalidTestValue(t *testing.T) {
30993099
require.Equal(t, "-1.0", invalidTestValue("number"))
31003100
require.Equal(t, `"__invalid__"`, invalidTestValue("object"))
31013101
}
3102+
3103+
func TestExtractValidators_ArrayValidators(t *testing.T) {
3104+
minItems := 1
3105+
maxItems := 10
3106+
3107+
tests := []struct {
3108+
name string
3109+
metadata *ConfigMetadata
3110+
expected []Validator
3111+
}{
3112+
{
3113+
name: "minItems only",
3114+
metadata: &ConfigMetadata{
3115+
Type: "object",
3116+
Properties: map[string]*ConfigMetadata{
3117+
"tags": {Type: "array", Items: &ConfigMetadata{Type: "string"}, MinItems: &minItems},
3118+
},
3119+
},
3120+
expected: []Validator{
3121+
{
3122+
FieldName: "tags",
3123+
FieldType: "array",
3124+
Rules: ValidationRules{MinItems: &minItems},
3125+
},
3126+
},
3127+
},
3128+
{
3129+
name: "maxItems only",
3130+
metadata: &ConfigMetadata{
3131+
Type: "object",
3132+
Properties: map[string]*ConfigMetadata{
3133+
"tags": {Type: "array", Items: &ConfigMetadata{Type: "string"}, MaxItems: &maxItems},
3134+
},
3135+
},
3136+
expected: []Validator{
3137+
{
3138+
FieldName: "tags",
3139+
FieldType: "array",
3140+
Rules: ValidationRules{MaxItems: &maxItems},
3141+
},
3142+
},
3143+
},
3144+
{
3145+
name: "uniqueItems only",
3146+
metadata: &ConfigMetadata{
3147+
Type: "object",
3148+
Properties: map[string]*ConfigMetadata{
3149+
"tags": {Type: "array", Items: &ConfigMetadata{Type: "string"}, UniqueItems: true},
3150+
},
3151+
},
3152+
expected: []Validator{
3153+
{
3154+
FieldName: "tags",
3155+
FieldType: "array",
3156+
Rules: ValidationRules{UniqueItems: true, ItemGoType: "string"},
3157+
},
3158+
},
3159+
},
3160+
{
3161+
name: "uniqueItems with integer items",
3162+
metadata: &ConfigMetadata{
3163+
Type: "object",
3164+
Properties: map[string]*ConfigMetadata{
3165+
"ids": {Type: "array", Items: &ConfigMetadata{Type: "integer"}, UniqueItems: true},
3166+
},
3167+
},
3168+
expected: []Validator{
3169+
{
3170+
FieldName: "ids",
3171+
FieldType: "array",
3172+
Rules: ValidationRules{UniqueItems: true, ItemGoType: "int"},
3173+
},
3174+
},
3175+
},
3176+
{
3177+
name: "uniqueItems without items schema falls back to any",
3178+
metadata: &ConfigMetadata{
3179+
Type: "object",
3180+
Properties: map[string]*ConfigMetadata{
3181+
"tags": {Type: "array", UniqueItems: true},
3182+
},
3183+
},
3184+
expected: []Validator{
3185+
{
3186+
FieldName: "tags",
3187+
FieldType: "array",
3188+
Rules: ValidationRules{UniqueItems: true},
3189+
},
3190+
},
3191+
},
3192+
{
3193+
name: "contains with enum",
3194+
metadata: &ConfigMetadata{
3195+
Type: "object",
3196+
Properties: map[string]*ConfigMetadata{
3197+
"tags": {
3198+
Type: "array",
3199+
Items: &ConfigMetadata{Type: "string"},
3200+
Contains: &ConfigMetadata{Type: "string", Enum: []any{"production", "staging"}},
3201+
},
3202+
},
3203+
},
3204+
expected: []Validator{
3205+
{
3206+
FieldName: "tags",
3207+
FieldType: "array",
3208+
Rules: ValidationRules{ContainsEnum: []any{"production", "staging"}, ItemFieldType: "string"},
3209+
},
3210+
},
3211+
},
3212+
{
3213+
name: "all array validators combined",
3214+
metadata: &ConfigMetadata{
3215+
Type: "object",
3216+
Properties: map[string]*ConfigMetadata{
3217+
"tags": {
3218+
Type: "array",
3219+
Items: &ConfigMetadata{Type: "string"},
3220+
MinItems: &minItems,
3221+
MaxItems: &maxItems,
3222+
UniqueItems: true,
3223+
Contains: &ConfigMetadata{Type: "string", Enum: []any{"production"}},
3224+
},
3225+
},
3226+
},
3227+
expected: []Validator{
3228+
{
3229+
FieldName: "tags",
3230+
FieldType: "array",
3231+
Rules: ValidationRules{
3232+
MinItems: &minItems,
3233+
MaxItems: &maxItems,
3234+
UniqueItems: true,
3235+
ItemGoType: "string",
3236+
ContainsEnum: []any{"production"},
3237+
ItemFieldType: "string",
3238+
},
3239+
},
3240+
},
3241+
},
3242+
{
3243+
name: "no array validators produces no validator",
3244+
metadata: &ConfigMetadata{
3245+
Type: "object",
3246+
Properties: map[string]*ConfigMetadata{
3247+
"tags": {Type: "array", Items: &ConfigMetadata{Type: "string"}},
3248+
},
3249+
},
3250+
expected: []Validator{},
3251+
},
3252+
{
3253+
name: "required combined with array validators",
3254+
metadata: &ConfigMetadata{
3255+
Type: "object",
3256+
Required: []string{"tags"},
3257+
Properties: map[string]*ConfigMetadata{
3258+
"tags": {Type: "array", Items: &ConfigMetadata{Type: "string"}, MinItems: &minItems},
3259+
},
3260+
},
3261+
expected: []Validator{
3262+
{
3263+
FieldName: "tags",
3264+
FieldType: "array",
3265+
Rules: ValidationRules{Required: true, MinItems: &minItems},
3266+
},
3267+
},
3268+
},
3269+
{
3270+
name: "array validators on pointer array",
3271+
metadata: &ConfigMetadata{
3272+
Type: "object",
3273+
Properties: map[string]*ConfigMetadata{
3274+
"targets": {Type: "array", Items: &ConfigMetadata{Type: "string"}, IsPointer: true, MinItems: &minItems, MaxItems: &maxItems},
3275+
},
3276+
},
3277+
expected: []Validator{
3278+
{
3279+
FieldName: "targets",
3280+
FieldType: "array",
3281+
IsPointer: true,
3282+
Rules: ValidationRules{MinItems: &minItems, MaxItems: &maxItems},
3283+
},
3284+
},
3285+
},
3286+
{
3287+
name: "contains without enum produces no containsEnum rule",
3288+
metadata: &ConfigMetadata{
3289+
Type: "object",
3290+
Properties: map[string]*ConfigMetadata{
3291+
"tags": {
3292+
Type: "array",
3293+
Items: &ConfigMetadata{Type: "string"},
3294+
Contains: &ConfigMetadata{Type: "string"},
3295+
},
3296+
},
3297+
},
3298+
expected: []Validator{},
3299+
},
3300+
}
3301+
3302+
for _, tt := range tests {
3303+
t.Run(tt.name, func(t *testing.T) {
3304+
result := ExtractValidators(tt.metadata)
3305+
require.Equal(t, tt.expected, result)
3306+
})
3307+
}
3308+
}
3309+
3310+
func TestValidationRules_HasValueRule_ArrayValidators(t *testing.T) {
3311+
tests := []struct {
3312+
name string
3313+
rules ValidationRules
3314+
expected bool
3315+
}{
3316+
{
3317+
name: "minItems",
3318+
rules: ValidationRules{MinItems: Ptr(1)},
3319+
expected: true,
3320+
},
3321+
{
3322+
name: "maxItems",
3323+
rules: ValidationRules{MaxItems: Ptr(10)},
3324+
expected: true,
3325+
},
3326+
{
3327+
name: "uniqueItems",
3328+
rules: ValidationRules{UniqueItems: true},
3329+
expected: true,
3330+
},
3331+
{
3332+
name: "containsEnum",
3333+
rules: ValidationRules{ContainsEnum: []any{"a"}},
3334+
expected: true,
3335+
},
3336+
{
3337+
name: "all array validators",
3338+
rules: ValidationRules{MinItems: Ptr(1), MaxItems: Ptr(10), UniqueItems: true, ContainsEnum: []any{"a"}},
3339+
expected: true,
3340+
},
3341+
}
3342+
3343+
for _, tt := range tests {
3344+
t.Run(tt.name, func(t *testing.T) {
3345+
require.Equal(t, tt.expected, tt.rules.HasValueRule())
3346+
})
3347+
}
3348+
}

cmd/mdatagen/internal/samplescraper/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ This scraper is used for testing purposes to check the output of mdatagen.
2929
| `log_level` | string (one of: debug, info, warn, error) | info | no | Logging level for the scraper. |
3030
| `metrics` | object (see [metrics](#metrics)) | | no | MetricsConfig provides config for sample metrics. |
3131
| `resource_attributes` | object (see [resource_attributes](#resource_attributes)) | | no | ResourceAttributesConfig provides config for sample resource attributes. |
32+
| `tags` | []string | [production] | no | Tags for the scraper instance. |
3233
| `targets` | []object | [{}] | **yes** | List of targets to scrape metrics from. |
3334
| `timeout` | duration | 0 | no | An optional value used to set scraper's context deadline. |
3435

cmd/mdatagen/internal/samplescraper/config.schema.json

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@
2424
"error"
2525
]
2626
},
27+
"tags": {
28+
"type": "array",
29+
"items": {
30+
"type": "string"
31+
},
32+
"description": "Tags for the scraper instance.",
33+
"default": [
34+
"production"
35+
],
36+
"uniqueItems": true,
37+
"contains": {
38+
"type": "string",
39+
"enum": [
40+
"production",
41+
"staging"
42+
]
43+
}
44+
},
2745
"targets": {
2846
"type": "array",
2947
"items": {
@@ -67,7 +85,9 @@
6785
"description": "List of targets to scrape metrics from.",
6886
"default": [
6987
{}
70-
]
88+
],
89+
"minItems": 1,
90+
"maxItems": 100
7191
}
7292
},
7393
"$id": "go.opentelemetry.io/collector/cmd/mdatagen/internal/samplescraper",

0 commit comments

Comments
 (0)