diff --git a/pkg/generators/markers.go b/pkg/generators/markers.go index 9b568a1a8..7421f6c41 100644 --- a/pkg/generators/markers.go +++ b/pkg/generators/markers.go @@ -62,8 +62,20 @@ func (c *CELTag) Validate() error { return nil } -// CommentTags represents the parsed comment tags for a given type. These types are then used to generate schema validations. -type CommentTags struct { +// commentTags represents the parsed comment tags for a given type. These types are then used to generate schema validations. +// These only include the newer prefixed tags. The older tags are still supported, +// but are not included in this struct. Comment Tags are transformed into a +// *spec.Schema, which is then combined with the older marker comments to produce +// the generated OpenAPI spec. +// +// List of tags not included in this struct: +// +// - +optional +// - +default +// - +listType +// - +listMapKeys +// - +mapType +type commentTags struct { spec.SchemaProps CEL []CELTag `json:"cel,omitempty"` @@ -73,8 +85,32 @@ type CommentTags struct { // Default any `json:"default,omitempty"` } +// Returns the schema for the given CommentTags instance. +// This is the final authoritative schema for the comment tags +func (c commentTags) ValidationSchema() (*spec.Schema, error) { + res := spec.Schema{ + SchemaProps: c.SchemaProps, + } + + if len(c.CEL) > 0 { + // Convert the CELTag to a map[string]interface{} via JSON + celTagJSON, err := json.Marshal(c.CEL) + if err != nil { + return nil, fmt.Errorf("failed to marshal CEL tag: %w", err) + } + var celTagMap []interface{} + if err := json.Unmarshal(celTagJSON, &celTagMap); err != nil { + return nil, fmt.Errorf("failed to unmarshal CEL tag: %w", err) + } + + res.VendorExtensible.AddExtension("x-kubernetes-validations", celTagMap) + } + + return &res, nil +} + // validates the parameters in a CommentTags instance. Returns any errors encountered. -func (c CommentTags) Validate() error { +func (c commentTags) Validate() error { var err error @@ -133,60 +169,63 @@ func (c CommentTags) Validate() error { } // Performs type-specific validation for CommentTags porameters. Accepts a Type instance and returns any errors encountered during validation. -func (c CommentTags) ValidateType(t *types.Type) error { +func (c commentTags) ValidateType(t *types.Type) error { var err error resolvedType := resolveAliasAndPtrType(t) typeString, _ := openapi.OpenAPITypeFormat(resolvedType.String()) // will be empty for complicated types - isNoValidate := resolvedType.Kind == types.Interface || resolvedType.Kind == types.Struct - if !isNoValidate { + // Structs and interfaces may dynamically be any type, so we cant validate them + // easily. We may be able to if we check that they don't implement all the + // override functions, but for now we just skip them. + if resolvedType.Kind == types.Interface || resolvedType.Kind == types.Struct { + return nil + } - isArray := resolvedType.Kind == types.Slice || resolvedType.Kind == types.Array - isMap := resolvedType.Kind == types.Map - isString := typeString == "string" - isInt := typeString == "integer" - isFloat := typeString == "number" + isArray := resolvedType.Kind == types.Slice || resolvedType.Kind == types.Array + isMap := resolvedType.Kind == types.Map + isString := typeString == "string" + isInt := typeString == "integer" + isFloat := typeString == "number" - if c.MaxItems != nil && !isArray { - err = errors.Join(err, fmt.Errorf("maxItems can only be used on array types")) - } - if c.MinItems != nil && !isArray { - err = errors.Join(err, fmt.Errorf("minItems can only be used on array types")) - } - if c.UniqueItems && !isArray { - err = errors.Join(err, fmt.Errorf("uniqueItems can only be used on array types")) - } - if c.MaxProperties != nil && !isMap { - err = errors.Join(err, fmt.Errorf("maxProperties can only be used on map types")) - } - if c.MinProperties != nil && !isMap { - err = errors.Join(err, fmt.Errorf("minProperties can only be used on map types")) - } - if c.MinLength != nil && !isString { - err = errors.Join(err, fmt.Errorf("minLength can only be used on string types")) - } - if c.MaxLength != nil && !isString { - err = errors.Join(err, fmt.Errorf("maxLength can only be used on string types")) - } - if c.Pattern != "" && !isString { - err = errors.Join(err, fmt.Errorf("pattern can only be used on string types")) - } - if c.Minimum != nil && !isInt && !isFloat { - err = errors.Join(err, fmt.Errorf("minimum can only be used on numeric types")) - } - if c.Maximum != nil && !isInt && !isFloat { - err = errors.Join(err, fmt.Errorf("maximum can only be used on numeric types")) - } - if c.MultipleOf != nil && !isInt && !isFloat { - err = errors.Join(err, fmt.Errorf("multipleOf can only be used on numeric types")) - } - if c.ExclusiveMinimum && !isInt && !isFloat { - err = errors.Join(err, fmt.Errorf("exclusiveMinimum can only be used on numeric types")) - } - if c.ExclusiveMaximum && !isInt && !isFloat { - err = errors.Join(err, fmt.Errorf("exclusiveMaximum can only be used on numeric types")) - } + if c.MaxItems != nil && !isArray { + err = errors.Join(err, fmt.Errorf("maxItems can only be used on array types")) + } + if c.MinItems != nil && !isArray { + err = errors.Join(err, fmt.Errorf("minItems can only be used on array types")) + } + if c.UniqueItems && !isArray { + err = errors.Join(err, fmt.Errorf("uniqueItems can only be used on array types")) + } + if c.MaxProperties != nil && !isMap { + err = errors.Join(err, fmt.Errorf("maxProperties can only be used on map types")) + } + if c.MinProperties != nil && !isMap { + err = errors.Join(err, fmt.Errorf("minProperties can only be used on map types")) + } + if c.MinLength != nil && !isString { + err = errors.Join(err, fmt.Errorf("minLength can only be used on string types")) + } + if c.MaxLength != nil && !isString { + err = errors.Join(err, fmt.Errorf("maxLength can only be used on string types")) + } + if c.Pattern != "" && !isString { + err = errors.Join(err, fmt.Errorf("pattern can only be used on string types")) + } + if c.Minimum != nil && !isInt && !isFloat { + err = errors.Join(err, fmt.Errorf("minimum can only be used on numeric types")) + } + if c.Maximum != nil && !isInt && !isFloat { + err = errors.Join(err, fmt.Errorf("maximum can only be used on numeric types")) + } + if c.MultipleOf != nil && !isInt && !isFloat { + err = errors.Join(err, fmt.Errorf("multipleOf can only be used on numeric types")) + } + if c.ExclusiveMinimum && !isInt && !isFloat { + err = errors.Join(err, fmt.Errorf("exclusiveMinimum can only be used on numeric types")) + } + if c.ExclusiveMaximum && !isInt && !isFloat { + err = errors.Join(err, fmt.Errorf("exclusiveMaximum can only be used on numeric types")) } return err @@ -196,27 +235,27 @@ func (c CommentTags) ValidateType(t *types.Type) error { // Accepts an optional type to validate against, and a prefix to filter out markers not related to validation. // Accepts a prefix to filter out markers not related to validation. // Returns any errors encountered while parsing or validating the comment tags. -func ParseCommentTags(t *types.Type, comments []string, prefix string) (CommentTags, error) { +func ParseCommentTags(t *types.Type, comments []string, prefix string) (*spec.Schema, error) { markers, err := parseMarkers(comments, prefix) if err != nil { - return CommentTags{}, fmt.Errorf("failed to parse marker comments: %w", err) + return nil, fmt.Errorf("failed to parse marker comments: %w", err) } nested, err := nestMarkers(markers) if err != nil { - return CommentTags{}, fmt.Errorf("invalid marker comments: %w", err) + return nil, fmt.Errorf("invalid marker comments: %w", err) } // Parse the map into a CommentTags type by marshalling and unmarshalling // as JSON in leiu of an unstructured converter. out, err := json.Marshal(nested) if err != nil { - return CommentTags{}, fmt.Errorf("failed to marshal marker comments: %w", err) + return nil, fmt.Errorf("failed to marshal marker comments: %w", err) } - var commentTags CommentTags + var commentTags commentTags if err = json.Unmarshal(out, &commentTags); err != nil { - return CommentTags{}, fmt.Errorf("failed to unmarshal marker comments: %w", err) + return nil, fmt.Errorf("failed to unmarshal marker comments: %w", err) } // Validate the parsed comment tags @@ -227,10 +266,10 @@ func ParseCommentTags(t *types.Type, comments []string, prefix string) (CommentT } if validationErrors != nil { - return CommentTags{}, fmt.Errorf("invalid marker comments: %w", validationErrors) + return nil, fmt.Errorf("invalid marker comments: %w", validationErrors) } - return commentTags, nil + return commentTags.ValidationSchema() } var ( diff --git a/pkg/generators/markers_test.go b/pkg/generators/markers_test.go index 1b7260b39..284996ca2 100644 --- a/pkg/generators/markers_test.go +++ b/pkg/generators/markers_test.go @@ -35,7 +35,7 @@ func TestParseCommentTags(t *testing.T) { t *types.Type name string comments []string - expected generators.CommentTags + expected *spec.Schema // regex pattern matching the error, or empty string/unset if no error // is expected @@ -59,7 +59,7 @@ func TestParseCommentTags(t *testing.T) { "exclusiveMaximum=true", "not+k8s:validation:Minimum=0.0", }, - expected: generators.CommentTags{ + expected: &spec.Schema{ SchemaProps: spec.SchemaProps{ Maximum: ptr.To(20.0), Minimum: ptr.To(10.0), @@ -74,8 +74,9 @@ func TestParseCommentTags(t *testing.T) { }, }, { - t: structKind, - name: "empty", + t: structKind, + name: "empty", + expected: &spec.Schema{}, }, { t: types.Float64, @@ -83,7 +84,7 @@ func TestParseCommentTags(t *testing.T) { comments: []string{ "+k8s:validation:minimum=10.0", }, - expected: generators.CommentTags{ + expected: &spec.Schema{ SchemaProps: spec.SchemaProps{ Minimum: ptr.To(10.0), }, @@ -96,7 +97,7 @@ func TestParseCommentTags(t *testing.T) { "+k8s:validation:minimum=10.0", "+k8s:validation:maximum=20.0", }, - expected: generators.CommentTags{ + expected: &spec.Schema{ SchemaProps: spec.SchemaProps{ Maximum: ptr.To(20.0), Minimum: ptr.To(10.0), @@ -119,6 +120,7 @@ func TestParseCommentTags(t *testing.T) { comments: []string{ "+ignored=30.0", }, + expected: &spec.Schema{}, }, { t: types.Float64, @@ -134,7 +136,7 @@ func TestParseCommentTags(t *testing.T) { comments: []string{ `+k8s:validation:minimum="asdf"`, }, - expectedError: `failed to unmarshal marker comments: json: cannot unmarshal string into Go struct field CommentTags.minimum of type float64`, + expectedError: `failed to unmarshal marker comments: json: cannot unmarshal string into Go struct field commentTags.minimum of type float64`, }, { @@ -152,6 +154,7 @@ func TestParseCommentTags(t *testing.T) { comments: []string{ "+k8s:validation:pattern=ref(asdf)", }, + expected: &spec.Schema{}, }, { t: types.Float64, @@ -160,11 +163,15 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule="oldSelf == self"`, `+k8s:validation:cel[0]:message="immutable field"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "oldSelf == self", - Message: "immutable field", + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "oldSelf == self", + "message": "immutable field", + }, + }, }, }, }, @@ -192,16 +199,20 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[1]:optionalOldSelf=true`, `+k8s:validation:cel[1]:message="must be greater than 5"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "oldSelf == self", - Message: "immutable field", - }, - { - Rule: "self > 5", - Message: "must be greater than 5", - OptionalOldSelf: ptr.To(true), + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "oldSelf == self", + "message": "immutable field", + }, + map[string]interface{}{ + "rule": "self > 5", + "optionalOldSelf": true, + "message": "must be greater than 5", + }, + }, }, }, }, @@ -217,17 +228,21 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[1]:optionalOldSelf=true`, `+k8s:validation:cel[1]:message="must be greater than 5"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "oldSelf == self", - MessageExpression: "self + ' must be equal to old value'", - OptionalOldSelf: ptr.To(true), - }, - { - Rule: "self > 5", - Message: "must be greater than 5", - OptionalOldSelf: ptr.To(true), + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "oldSelf == self", + "optionalOldSelf": true, + "messageExpression": "self + ' must be equal to old value'", + }, + map[string]interface{}{ + "rule": "self > 5", + "optionalOldSelf": true, + "message": "must be greater than 5", + }, + }, }, }, }, @@ -304,15 +319,19 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule="string rule [1]"`, `+k8s:validation:pattern="self[3] == 'hi'"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "string rule [1]", - Message: "[3]string rule [1]", - }, - }, + expected: &spec.Schema{ SchemaProps: spec.SchemaProps{ - Pattern: "self[3] == 'hi'", + Pattern: `self[3] == 'hi'`, + }, + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "string rule [1]", + "message": "[3]string rule [1]", + }, + }, + }, }, }, }, @@ -324,16 +343,20 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule> raw string rule [1]`, `+k8s:validation:pattern>"self[3] == 'hi'"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "raw string rule [1]", - Message: "[3]raw string message with subscirpt [3]\"", - }, - }, + expected: &spec.Schema{ SchemaProps: spec.SchemaProps{ Pattern: `"self[3] == 'hi'"`, }, + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "raw string rule [1]", + "message": "[3]raw string message with subscirpt [3]\"", + }, + }, + }, + }, }, }, { @@ -369,16 +392,20 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[1]:message="must be greater than 5"`, `+k8s:validation:cel[1]:optionalOldSelf`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "oldSelf == self", - Message: "cant change", - }, - { - Rule: "self > 5", - Message: "must be greater than 5", - OptionalOldSelf: ptr.To(true), + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "oldSelf == self", + "message": "cant change", + }, + map[string]interface{}{ + "rule": "self > 5", + "message": "must be greater than 5", + "optionalOldSelf": true, + }, + }, }, }, }, @@ -390,11 +417,15 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule> raw string rule`, `+k8s:validation:cel[0]:message="raw string message"`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "raw string rule", - Message: "raw string message", + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "raw string rule", + "message": "raw string message", + }, + }, }, }, }, @@ -408,11 +439,15 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule> : self.field == self.name + ' is odd'`, `+k8s:validation:cel[0]:message>raw string message`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "self.length() % 2 == 0\n? self.field == self.name + ' is even'\n: self.field == self.name + ' is odd'", - Message: "raw string message", + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "self.length() % 2 == 0\n? self.field == self.name + ' is even'\n: self.field == self.name + ' is odd'", + "message": "raw string message", + }, + }, }, }, }, @@ -426,11 +461,15 @@ func TestParseCommentTags(t *testing.T) { `+k8s:validation:cel[0]:rule> ? self.field == self.name + ' is even'`, `+k8s:validation:cel[0]:rule> : self.field == self.name + ' is odd'`, }, - expected: generators.CommentTags{ - CEL: []generators.CELTag{ - { - Rule: "self.length() % 2 == 0\n? self.field == self.name + ' is even'\n: self.field == self.name + ' is odd'", - Message: "raw string message", + expected: &spec.Schema{ + VendorExtensible: spec.VendorExtensible{ + Extensions: map[string]interface{}{ + "x-kubernetes-validations": []interface{}{ + map[string]interface{}{ + "rule": "self.length() % 2 == 0\n? self.field == self.name + ' is even'\n: self.field == self.name + ' is odd'", + "message": "raw string message", + }, + }, }, }, }, diff --git a/pkg/generators/openapi.go b/pkg/generators/openapi.go index 757009251..dbef0916c 100644 --- a/pkg/generators/openapi.go +++ b/pkg/generators/openapi.go @@ -412,6 +412,7 @@ func (g openAPITypeWriter) generateValueValidations(vs *spec.SchemaProps) error if vs.UniqueItems { g.Do("UniqueItems: true,\n", nil) } + return nil } @@ -419,7 +420,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { // Only generate for struct type and ignore the rest switch t.Kind { case types.Struct: - overrides, err := ParseCommentTags(t, t.CommentLines, markerPrefix) + validationSchema, err := ParseCommentTags(t, t.CommentLines, markerPrefix) if err != nil { return err } @@ -444,12 +445,12 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.generateDescription(t.CommentLines) g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, overrides); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { return err } g.Do("},\n", nil) @@ -463,12 +464,12 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.generateDescription(t.CommentLines) g.Do("OneOf:common.GenerateOpenAPIV3OneOfSchema($.type|raw${}.OpenAPIV3OneOfTypes()),\n"+ "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, overrides); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { return err } g.Do("},\n", nil) @@ -480,12 +481,12 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.generateDescription(t.CommentLines) g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, overrides); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { return err } g.Do("},\n", nil) @@ -498,12 +499,12 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.generateDescription(t.CommentLines) g.Do("Type:$.type|raw${}.OpenAPISchemaType(),\n"+ "Format:$.type|raw${}.OpenAPISchemaFormat(),\n", args) - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, overrides); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { return err } g.Do("},\n", nil) @@ -517,7 +518,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.Do("return $.OpenAPIDefinition|raw${\nSchema: spec.Schema{\nSchemaProps: spec.SchemaProps{\n", args) g.generateDescription(t.CommentLines) g.Do("Type: []string{\"object\"},\n", nil) - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } @@ -541,7 +542,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { g.Do("Required: []string{\"$.$\"},\n", strings.Join(required, "\",\"")) } g.Do("},\n", nil) - if err := g.generateStructExtensions(t, overrides); err != nil { + if err := g.generateStructExtensions(t, validationSchema.Extensions); err != nil { return err } g.Do("},\n", nil) @@ -574,7 +575,7 @@ func (g openAPITypeWriter) generate(t *types.Type) error { return nil } -func (g openAPITypeWriter) generateStructExtensions(t *types.Type, tags CommentTags) error { +func (g openAPITypeWriter) generateStructExtensions(t *types.Type, otherExtensions map[string]interface{}) error { extensions, errors := parseExtensions(t.CommentLines) // Initially, we will only log struct extension errors. if len(errors) > 0 { @@ -590,11 +591,11 @@ func (g openAPITypeWriter) generateStructExtensions(t *types.Type, tags CommentT } // TODO(seans3): Validate struct extensions here. - g.emitExtensions(extensions, unions, tags.CEL) + g.emitExtensions(extensions, unions, otherExtensions) return nil } -func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *types.Type, tags CommentTags) error { +func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *types.Type, otherExtensions map[string]interface{}) error { extensions, parseErrors := parseExtensions(m.CommentLines) validationErrors := validateMemberExtensions(extensions, m) errors := append(parseErrors, validationErrors...) @@ -605,13 +606,13 @@ func (g openAPITypeWriter) generateMemberExtensions(m *types.Member, parent *typ klog.V(2).Infof("%s %s\n", errorPrefix, e) } } - g.emitExtensions(extensions, nil, tags.CEL) + g.emitExtensions(extensions, nil, otherExtensions) return nil } -func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union, celRules []CELTag) { +func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union, otherExtensions map[string]interface{}) { // If any extensions exist, then emit code to create them. - if len(extensions) == 0 && len(unions) == 0 && len(celRules) == 0 { + if len(extensions) == 0 && len(unions) == 0 && len(otherExtensions) == 0 { return } g.Do("VendorExtensible: spec.VendorExtensible{\nExtensions: spec.Extensions{\n", nil) @@ -635,42 +636,13 @@ func (g openAPITypeWriter) emitExtensions(extensions []extension, unions []union g.Do("},\n", nil) } - if len(celRules) > 0 { - g.Do("\"x-kubernetes-validations\": []interface{}{\n", nil) - for _, rule := range celRules { - g.Do("map[string]interface{}{\n", nil) - - g.Do("\"rule\": $.$,\n", fmt.Sprintf("%#v", rule.Rule)) - - if len(rule.Message) > 0 { - g.Do("\"message\": $.$,\n", fmt.Sprintf("%#v", rule.Message)) - } - - if len(rule.MessageExpression) > 0 { - g.Do("\"messageExpression\": $.$,\n", fmt.Sprintf("%#v", rule.MessageExpression)) - } - - if rule.OptionalOldSelf != nil && *rule.OptionalOldSelf { - g.Do("\"optionalOldSelf\": $.ptrTo|raw$[bool](true),\n", generator.Args{ - "ptrTo": &types.Type{ - Name: types.Name{ - Package: "k8s.io/utils/ptr", - Name: "To", - }}, - }) - } - - if len(rule.Reason) > 0 { - g.Do("\"reason\": $.$,\n", fmt.Sprintf("%#v", rule.Reason)) - } - - if len(rule.FieldPath) > 0 { - g.Do("\"fieldPath\": $.$,\n", fmt.Sprintf("%#v", rule.FieldPath)) - } - - g.Do("},\n", nil) + if len(otherExtensions) > 0 { + for k, v := range otherExtensions { + g.Do("$.key$: $.value$,\n", map[string]interface{}{ + "key": fmt.Sprintf("%#v", k), + "value": fmt.Sprintf("%#v", v), + }) } - g.Do("},\n", nil) } g.Do("},\n},\n", nil) @@ -857,7 +829,7 @@ func (g openAPITypeWriter) generateProperty(m *types.Member, parent *types.Type) if name == "" { return nil } - overrides, err := ParseCommentTags(m.Type, m.CommentLines, markerPrefix) + validationSchema, err := ParseCommentTags(m.Type, m.CommentLines, markerPrefix) if err != nil { return err } @@ -865,7 +837,7 @@ func (g openAPITypeWriter) generateProperty(m *types.Member, parent *types.Type) return err } g.Do("\"$.$\": {\n", name) - if err := g.generateMemberExtensions(m, parent, overrides); err != nil { + if err := g.generateMemberExtensions(m, parent, validationSchema.Extensions); err != nil { return err } g.Do("SchemaProps: spec.SchemaProps{\n", nil) @@ -884,7 +856,7 @@ func (g openAPITypeWriter) generateProperty(m *types.Member, parent *types.Type) if err := g.generateDefault(m.CommentLines, m.Type, omitEmpty, parent); err != nil { return fmt.Errorf("failed to generate default in %v: %v: %v", parent, m.Name, err) } - err = g.generateValueValidations(&overrides.SchemaProps) + err = g.generateValueValidations(&validationSchema.SchemaProps) if err != nil { return err } diff --git a/pkg/generators/openapi_test.go b/pkg/generators/openapi_test.go index 2ae47b227..c268829be 100644 --- a/pkg/generators/openapi_test.go +++ b/pkg/generators/openapi_test.go @@ -2045,7 +2045,7 @@ func TestCELMarkerComments(t *testing.T) { assert.NoError(funcErr) assert.NoError(callErr) - assert.ElementsMatch(imports, []string{`foo "base/foo"`, `common "k8s.io/kube-openapi/pkg/common"`, `spec "k8s.io/kube-openapi/pkg/validation/spec"`, `ptr "k8s.io/utils/ptr"`}) + assert.ElementsMatch(imports, []string{`foo "base/foo"`, `common "k8s.io/kube-openapi/pkg/common"`, `spec "k8s.io/kube-openapi/pkg/validation/spec"`}) if formatted, err := format.Source(funcBuffer.Bytes()); err != nil { t.Fatalf("%v\n%v", err, string(funcBuffer.Bytes())) @@ -2059,17 +2059,7 @@ func TestCELMarkerComments(t *testing.T) { "Field": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ - "x-kubernetes-validations": []interface{}{ - map[string]interface{}{ - "rule": "self.length() > 0", - "message": "string message", - }, - map[string]interface{}{ - "rule": "self.length() % 2 == 0", - "messageExpression": "self + ' hello'", - "optionalOldSelf": ptr.To[bool](true), - }, - }, + "x-kubernetes-validations": []interface{}{map[string]interface{}{"message": "string message", "rule": "self.length() > 0"}, map[string]interface{}{"messageExpression": "self + ' hello'", "optionalOldSelf": true, "rule": "self.length() % 2 == 0"}}, }, }, SchemaProps: spec.SchemaProps{ @@ -2082,12 +2072,7 @@ func TestCELMarkerComments(t *testing.T) { }, VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ - "x-kubernetes-validations": []interface{}{ - map[string]interface{}{ - "rule": "self == oldSelf", - "message": "message1", - }, - }, + "x-kubernetes-validations": []interface{}{map[string]interface{}{"message": "message1", "rule": "self == oldSelf"}}, }, }, }, @@ -2127,7 +2112,7 @@ func TestMultilineCELMarkerComments(t *testing.T) { assert.NoError(funcErr) assert.NoError(callErr) - assert.ElementsMatch(imports, []string{`foo "base/foo"`, `common "k8s.io/kube-openapi/pkg/common"`, `spec "k8s.io/kube-openapi/pkg/validation/spec"`, `ptr "k8s.io/utils/ptr"`}) + assert.ElementsMatch(imports, []string{`foo "base/foo"`, `common "k8s.io/kube-openapi/pkg/common"`, `spec "k8s.io/kube-openapi/pkg/validation/spec"`}) if formatted, err := format.Source(funcBuffer.Bytes()); err != nil { t.Fatalf("%v\n%v", err, string(funcBuffer.Bytes())) @@ -2141,18 +2126,7 @@ func TestMultilineCELMarkerComments(t *testing.T) { "Field": { VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ - "x-kubernetes-validations": []interface{}{ - map[string]interface{}{ - "rule": "self.length() > 0", - "message": "string message", - "reason": "Invalid", - }, - map[string]interface{}{ - "rule": "!oldSelf.hasValue() || self.length() % 2 == 0\n? self.field == \"even\"\n: self.field == \"odd\"", - "messageExpression": "field must be whether the length of the string is even or odd", - "optionalOldSelf": ptr.To[bool](true), - }, - }, + "x-kubernetes-validations": []interface{}{map[string]interface{}{"message": "string message", "reason": "Invalid", "rule": "self.length() > 0"}, map[string]interface{}{"messageExpression": "field must be whether the length of the string is even or odd", "optionalOldSelf": true, "rule": "!oldSelf.hasValue() || self.length() % 2 == 0\n? self.field == \"even\"\n: self.field == \"odd\""}}, }, }, SchemaProps: spec.SchemaProps{ @@ -2165,13 +2139,7 @@ func TestMultilineCELMarkerComments(t *testing.T) { }, VendorExtensible: spec.VendorExtensible{ Extensions: spec.Extensions{ - "x-kubernetes-validations": []interface{}{ - map[string]interface{}{ - "rule": "self == oldSelf", - "message": "message1", - "fieldPath": "field", - }, - }, + "x-kubernetes-validations": []interface{}{map[string]interface{}{"fieldPath": "field", "message": "message1", "rule": "self == oldSelf"}}, }, }, },