Skip to content

Commit 7e9f2b4

Browse files
authored
Merge pull request #661 from simonfuhrer/main
feat(simpleschema): add support for pattern, minLength, maxLengt, uniqueItems, maxItems and minItems markers
2 parents 1b53914 + d1182c0 commit 7e9f2b4

23 files changed

+1082
-3
lines changed

pkg/simpleschema/markers.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,26 @@ const (
5757
MarkerTypeEnum MarkerType = "enum"
5858
// MarkerTypeImmutable represents the `immutable` marker.
5959
MarkerTypeImmutable MarkerType = "immutable"
60+
// MarkerTypePattern represents the `pattern` marker.
61+
MarkerTypePattern MarkerType = "pattern"
62+
// MarkerTypeUniqueItems represents the `uniqueItems` marker.
63+
MarkerTypeUniqueItems MarkerType = "uniqueItems"
64+
// MarkerTypeMinLength represents the `minLength` marker.
65+
MarkerTypeMinLength MarkerType = "minLength"
66+
// MarkerTypeMaxLength represents the `maxLength` marker.
67+
MarkerTypeMaxLength MarkerType = "maxLength"
68+
// MarkerTypeMinItems represents the `minItems` marker.
69+
MarkerTypeMinItems MarkerType = "minItems"
70+
// MarkerTypeMaxItems represents the `maxItems` marker.
71+
MarkerTypeMaxItems MarkerType = "maxItems"
6072
)
6173

6274
func markerTypeFromString(s string) (MarkerType, error) {
6375
switch MarkerType(s) {
6476
case MarkerTypeRequired, MarkerTypeDefault, MarkerTypeDescription,
65-
MarkerTypeMinimum, MarkerTypeMaximum, MarkerTypeValidation, MarkerTypeEnum, MarkerTypeImmutable:
77+
MarkerTypeMinimum, MarkerTypeMaximum, MarkerTypeValidation, MarkerTypeEnum, MarkerTypeImmutable,
78+
MarkerTypePattern, MarkerTypeUniqueItems, MarkerTypeMinLength, MarkerTypeMaxLength, MarkerTypeMinItems,
79+
MarkerTypeMaxItems:
6680
return MarkerType(s), nil
6781
default:
6882
return "", fmt.Errorf("unknown marker type: %s", s)

pkg/simpleschema/markers_test.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,119 @@ func TestParseMarkers(t *testing.T) {
134134
},
135135
wantErr: false,
136136
},
137+
{
138+
name: "pattern marker",
139+
input: "pattern=\"^[a-zA-Z0-9]+$\"",
140+
want: []*Marker{
141+
{MarkerType: MarkerTypePattern, Key: "pattern", Value: "^[a-zA-Z0-9]+$"},
142+
},
143+
wantErr: false,
144+
},
145+
{
146+
name: "minLength and maxLength markers",
147+
input: "minLength=5 maxLength=20",
148+
want: []*Marker{
149+
{MarkerType: MarkerTypeMinLength, Key: "minLength", Value: "5"},
150+
{MarkerType: MarkerTypeMaxLength, Key: "maxLength", Value: "20"},
151+
},
152+
wantErr: false,
153+
},
154+
{
155+
name: "all string validation markers",
156+
input: "pattern=\"[a-z]+\" minLength=3 maxLength=15 description=\"String field with validation\"",
157+
want: []*Marker{
158+
{MarkerType: MarkerTypePattern, Key: "pattern", Value: "[a-z]+"},
159+
{MarkerType: MarkerTypeMinLength, Key: "minLength", Value: "3"},
160+
{MarkerType: MarkerTypeMaxLength, Key: "maxLength", Value: "15"},
161+
{MarkerType: MarkerTypeDescription, Key: "description", Value: "String field with validation"},
162+
},
163+
wantErr: false,
164+
},
165+
{
166+
name: "pattern with special regex characters",
167+
input: "pattern=\"^(foo|bar)\\d{2,4}$\"",
168+
want: []*Marker{
169+
{MarkerType: MarkerTypePattern, Key: "pattern", Value: "^(foo|bar)\\d{2,4}$"},
170+
},
171+
wantErr: false,
172+
},
173+
{
174+
name: "zero minLength",
175+
input: "minLength=0",
176+
want: []*Marker{
177+
{MarkerType: MarkerTypeMinLength, Key: "minLength", Value: "0"},
178+
},
179+
wantErr: false,
180+
},
181+
{
182+
name: "uniqueItems marker true",
183+
input: "uniqueItems=true",
184+
want: []*Marker{
185+
{MarkerType: MarkerTypeUniqueItems, Key: "uniqueItems", Value: "true"},
186+
},
187+
wantErr: false,
188+
},
189+
{
190+
name: "uniqueItems marker false",
191+
input: "uniqueItems=false",
192+
want: []*Marker{
193+
{MarkerType: MarkerTypeUniqueItems, Key: "uniqueItems", Value: "false"},
194+
},
195+
wantErr: false,
196+
},
197+
{
198+
name: "array field with uniqueItems and validation",
199+
input: "uniqueItems=true description=\"Array with unique elements\"",
200+
want: []*Marker{
201+
{MarkerType: MarkerTypeUniqueItems, Key: "uniqueItems", Value: "true"},
202+
{MarkerType: MarkerTypeDescription, Key: "description", Value: "Array with unique elements"},
203+
},
204+
wantErr: false,
205+
},
206+
{
207+
name: "minItems marker",
208+
input: "minItems=2",
209+
want: []*Marker{
210+
{MarkerType: MarkerTypeMinItems, Key: "minItems", Value: "2"},
211+
},
212+
wantErr: false,
213+
},
214+
{
215+
name: "maxItems marker",
216+
input: "maxItems=10",
217+
want: []*Marker{
218+
{MarkerType: MarkerTypeMaxItems, Key: "maxItems", Value: "10"},
219+
},
220+
wantErr: false,
221+
},
222+
{
223+
name: "minItems and maxItems markers",
224+
input: "minItems=1 maxItems=5",
225+
want: []*Marker{
226+
{MarkerType: MarkerTypeMinItems, Key: "minItems", Value: "1"},
227+
{MarkerType: MarkerTypeMaxItems, Key: "maxItems", Value: "5"},
228+
},
229+
wantErr: false,
230+
},
231+
{
232+
name: "array field with all validation markers",
233+
input: "uniqueItems=true minItems=2 maxItems=8 description=\"Array with comprehensive validation\"",
234+
want: []*Marker{
235+
{MarkerType: MarkerTypeUniqueItems, Key: "uniqueItems", Value: "true"},
236+
{MarkerType: MarkerTypeMinItems, Key: "minItems", Value: "2"},
237+
{MarkerType: MarkerTypeMaxItems, Key: "maxItems", Value: "8"},
238+
{MarkerType: MarkerTypeDescription, Key: "description", Value: "Array with comprehensive validation"},
239+
},
240+
wantErr: false,
241+
},
242+
{
243+
name: "zero minItems",
244+
input: "minItems=0",
245+
want: []*Marker{
246+
{MarkerType: MarkerTypeMinItems, Key: "minItems", Value: "0"},
247+
},
248+
wantErr: false,
249+
},
137250
}
138251

139252
for _, tt := range tests {

pkg/simpleschema/transform.go

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package simpleschema
1616

1717
import (
1818
"fmt"
19+
"regexp"
1920
"slices"
2021
"strconv"
2122
"strings"
@@ -30,6 +31,7 @@ const (
3031
keyTypeBoolean = string(AtomicTypeBool)
3132
keyTypeNumber = "number"
3233
keyTypeObject = "object"
34+
keyTypeArray = "array"
3335
)
3436

3537
// A predefinedType is a type that is predefined in the schema.
@@ -202,7 +204,7 @@ func (tf *transformer) handleSliceType(key, fieldType string) (*extv1.JSONSchema
202204
}
203205

204206
fieldJSONSchemaProps := &extv1.JSONSchemaProps{
205-
Type: "array",
207+
Type: keyTypeArray,
206208
Items: &extv1.JSONSchemaPropsOrArray{
207209
Schema: &extv1.JSONSchemaProps{},
208210
},
@@ -225,6 +227,7 @@ func (tf *transformer) handleSliceType(key, fieldType string) (*extv1.JSONSchema
225227
return fieldJSONSchemaProps, nil
226228
}
227229

230+
//nolint:gocyclo
228231
func (tf *transformer) applyMarkers(schema *extv1.JSONSchemaProps, markers []*Marker, key string, parentSchema *extv1.JSONSchemaProps) error {
229232
for _, marker := range markers {
230233
switch marker.MarkerType {
@@ -265,7 +268,7 @@ func (tf *transformer) applyMarkers(schema *extv1.JSONSchemaProps, markers []*Ma
265268
}
266269
schema.Maximum = &val
267270
case MarkerTypeValidation:
268-
if marker.Value == "" {
271+
if strings.TrimSpace(marker.Value) == "" {
269272
return fmt.Errorf("validation failed")
270273
}
271274
validation := []extv1.ValidationRule{
@@ -316,6 +319,75 @@ func (tf *transformer) applyMarkers(schema *extv1.JSONSchemaProps, markers []*Ma
316319
if len(enumJSONValues) > 0 {
317320
schema.Enum = enumJSONValues
318321
}
322+
case MarkerTypeMinLength:
323+
// MinLength is only valid for string types
324+
if schema.Type != keyTypeString {
325+
return fmt.Errorf("minLength marker is only valid for string types, got type: %s", schema.Type)
326+
}
327+
val, err := strconv.ParseInt(marker.Value, 10, 64)
328+
if err != nil {
329+
return fmt.Errorf("failed to parse minLength value: %w", err)
330+
}
331+
schema.MinLength = &val
332+
333+
case MarkerTypeMaxLength:
334+
// MaxLength is only valid for string types
335+
if schema.Type != keyTypeString {
336+
return fmt.Errorf("maxLength marker is only valid for string types, got type: %s", schema.Type)
337+
}
338+
val, err := strconv.ParseInt(marker.Value, 10, 64)
339+
if err != nil {
340+
return fmt.Errorf("failed to parse maxLength value: %w", err)
341+
}
342+
schema.MaxLength = &val
343+
case MarkerTypePattern:
344+
if marker.Value == "" {
345+
return fmt.Errorf("pattern marker value cannot be empty")
346+
}
347+
// Pattern is only valid for string types
348+
if schema.Type != keyTypeString {
349+
return fmt.Errorf("pattern marker is only valid for string types, got type: %s", schema.Type)
350+
}
351+
// Validate regex
352+
if _, err := regexp.Compile(marker.Value); err != nil {
353+
return fmt.Errorf("invalid pattern regex: %w", err)
354+
}
355+
schema.Pattern = marker.Value
356+
case MarkerTypeUniqueItems:
357+
// UniqueItems is only valid for array types
358+
switch isUnique, err := strconv.ParseBool(marker.Value); {
359+
case err != nil:
360+
return fmt.Errorf("failed to parse uniqueItems marker value: %w", err)
361+
case schema.Type != keyTypeArray:
362+
return fmt.Errorf("uniqueItems marker is only valid for array types, got type: %s", schema.Type)
363+
case isUnique:
364+
// Always set x-kubernetes-list-type to "set" when uniqueItems is true
365+
// https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions
366+
// https://stackoverflow.com/questions/79399232/forbidden-uniqueitems-cannot-be-set-to-true-since-the-runtime-complexity-become
367+
schema.XListType = ptr.To("set")
368+
default:
369+
// ignore
370+
}
371+
case MarkerTypeMinItems:
372+
// MinItems is only valid for array types
373+
if schema.Type != keyTypeArray {
374+
return fmt.Errorf("minItems marker is only valid for array types, got type: %s", schema.Type)
375+
}
376+
val, err := strconv.ParseInt(marker.Value, 10, 64)
377+
if err != nil {
378+
return fmt.Errorf("failed to parse minItems value: %w", err)
379+
}
380+
schema.MinItems = &val
381+
case MarkerTypeMaxItems:
382+
// MaxItems is only valid for array types
383+
if schema.Type != keyTypeArray {
384+
return fmt.Errorf("maxItems marker is only valid for array types, got type: %s", schema.Type)
385+
}
386+
val, err := strconv.ParseInt(marker.Value, 10, 64)
387+
if err != nil {
388+
return fmt.Errorf("failed to parse maxItems value: %w", err)
389+
}
390+
schema.MaxItems = &val
319391
}
320392
}
321393
return nil

0 commit comments

Comments
 (0)