Skip to content

Commit

Permalink
Merge pull request #904 from carvel-dev/dk-ytt-openapi
Browse files Browse the repository at this point in the history
Refactor openapi validation gen to avoid mutation inside GetValueType()
  • Loading branch information
joaopapereira authored Apr 15, 2024
2 parents d8e4013 + 9663687 commit 2e686ef
Show file tree
Hide file tree
Showing 5 changed files with 271 additions and 135 deletions.
105 changes: 105 additions & 0 deletions pkg/cmd/template/schema_inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,9 @@ foo:
#@schema/validation min=0, max=100
range_key: 0
#@schema/validation min=-1.1, max=100.1
range_float_key: 2.2
#@schema/default 10
#@schema/validation min=0
min_key: 0
Expand All @@ -564,11 +567,22 @@ foo:
#@schema/validation min_len=1, max_len=10
string_key: ""
#@schema/validation min_len=3, max_len=4
array_key:
- ""
#@schema/validation min_len=2, max_len=5
map_key: {}
#@schema/validation one_of=[1,2,3]
one_of_integers: 1
#@schema/validation one_of=["one", "two", "three"]
one_of_strings: "one"
#@schema/type any=True
#@schema/validation one_of=["one", 2, 3.3, {}]
one_of_mixed: "one"
`
expected := `openapi: 3.0.0
info:
Expand All @@ -590,6 +604,12 @@ components:
default: 10
minimum: 0
maximum: 100
range_float_key:
type: number
format: float
default: 2.2
minimum: -1.1
maximum: 100.1
min_key:
type: integer
default: 10
Expand All @@ -603,6 +623,20 @@ components:
default: ""
minLength: 1
maxLength: 10
array_key:
type: array
items:
type: string
default: ""
default: []
minItems: 3
maxItems: 4
map_key:
type: object
additionalProperties: false
properties: {}
minProperties: 2
maxProperties: 5
one_of_integers:
type: integer
default: 1
Expand All @@ -617,6 +651,14 @@ components:
- one
- two
- three
one_of_mixed:
nullable: true
default: one
enum:
- one
- 2
- 3.3
- {}
`

filesToProcess := files.NewSortedFiles([]*files.File{
Expand All @@ -626,6 +668,69 @@ components:
assertSucceedsDocSet(t, filesToProcess, expected, opts)
})

t.Run("not including named validations when when= is present", func(t *testing.T) {
opts := cmdtpl.NewOptions()
opts.DataValuesFlags.InspectSchema = true
opts.RegularFilesSourceOpts.OutputType.Types = []string{"openapi-v3"}

schemaYAML := `#@data/values-schema
---
foo:
#@schema/validation min=0, max=100, when=lambda: False
range_key: 0
#@schema/default 10
#@schema/validation min=0, when=lambda: False
min_key: 0
#@schema/default 10
#@schema/validation max=100, when=lambda: False
max_key: 0
#@schema/validation min_len=1, max_len=10, when=lambda: False
string_key: ""
#@schema/validation one_of=[1,2,3], when=lambda: False
one_of_integers: 1
`
expected := `openapi: 3.0.0
info:
version: 0.1.0
title: Schema for data values, generated by ytt
paths: {}
components:
schemas:
dataValues:
type: object
additionalProperties: false
properties:
foo:
type: object
additionalProperties: false
properties:
range_key:
type: integer
default: 0
min_key:
type: integer
default: 10
max_key:
type: integer
default: 10
string_key:
type: string
default: ""
one_of_integers:
type: integer
default: 1
`

filesToProcess := files.NewSortedFiles([]*files.File{
files.MustNewFileFromSource(files.NewBytesSource("schema.yml", []byte(schemaYAML))),
})

assertSucceedsDocSet(t, filesToProcess, expected, opts)
})
}
func TestSchemaInspect_annotation_adds_key(t *testing.T) {
t.Run("in the correct relative order", func(t *testing.T) {
Expand Down
115 changes: 81 additions & 34 deletions pkg/schema/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,12 @@ const (
defaultProp = "default"
minProp = "minimum"
maxProp = "maximum"
minLenProp = "minLength"
minLenProp = "minLength" // for strings
maxLenProp = "maxLength"
minItemsProp = "minItems" // for arrays
maxItemsProp = "maxItems"
minPropertiesProp = "minProperties" // for objects
maxPropertiesProp = "maxProperties"
enumProp = "enum"
)

Expand All @@ -48,22 +52,20 @@ var propOrder = map[string]int{
maxProp: 13,
minLenProp: 14,
maxLenProp: 15,
enumProp: 16,
minItemsProp: 16,
maxItemsProp: 17,
minPropertiesProp: 18,
maxPropertiesProp: 19,
enumProp: 20,
}

type openAPIKeys []*yamlmeta.MapItem

func (o openAPIKeys) Len() int {
return len(o)
}

func (o openAPIKeys) Len() int { return len(o) }
func (o openAPIKeys) Less(i, j int) bool {
return propOrder[o[i].Key.(string)] < propOrder[o[j].Key.(string)]
}

func (o openAPIKeys) Swap(i, j int) {
o[i], o[j] = o[j], o[i]
}
func (o openAPIKeys) Swap(i, j int) { o[i], o[j] = o[j], o[i] }

// OpenAPIDocument holds the document type used for creating an OpenAPI document
type OpenAPIDocument struct {
Expand Down Expand Up @@ -98,25 +100,38 @@ func (o *OpenAPIDocument) AsDocument() *yamlmeta.Document {
func (o *OpenAPIDocument) calculateProperties(schemaVal interface{}) *yamlmeta.Map {
switch typedValue := schemaVal.(type) {
case *DocumentType:
return o.calculateProperties(typedValue.GetValueType())
result := o.calculateProperties(typedValue.GetValueType())
result.Items = append(result.Items, o.convertValidations(typedValue)...)
sort.Sort(openAPIKeys(result.Items))
return result

case *MapType:
var items openAPIKeys
items = append(items, collectDocumentation(typedValue)...)
items = append(items, o.collectDocumentation(typedValue)...)
items = append(items, o.convertValidations(typedValue)...)
items = append(items, &yamlmeta.MapItem{Key: typeProp, Value: "object"})
items = append(items, &yamlmeta.MapItem{Key: additionalPropsProp, Value: false})

var properties []*yamlmeta.MapItem
for _, i := range typedValue.Items {
mi := yamlmeta.MapItem{Key: i.Key, Value: o.calculateProperties(i.GetValueType())}
mi := yamlmeta.MapItem{Key: i.Key, Value: o.calculateProperties(i)}
properties = append(properties, &mi)
}
items = append(items, &yamlmeta.MapItem{Key: propertiesProp, Value: &yamlmeta.Map{Items: properties}})

sort.Sort(items)
return &yamlmeta.Map{Items: items}

case *MapItemType:
result := o.calculateProperties(typedValue.GetValueType())
result.Items = append(result.Items, o.convertValidations(typedValue)...)
sort.Sort(openAPIKeys(result.Items))
return result

case *ArrayType:
var items openAPIKeys
items = append(items, collectDocumentation(typedValue)...)
items = append(items, o.collectDocumentation(typedValue)...)
items = append(items, o.convertValidations(typedValue)...)
items = append(items, &yamlmeta.MapItem{Key: typeProp, Value: "array"})
items = append(items, &yamlmeta.MapItem{Key: defaultProp, Value: typedValue.GetDefaultValue()})

Expand All @@ -126,46 +141,51 @@ func (o *OpenAPIDocument) calculateProperties(schemaVal interface{}) *yamlmeta.M

sort.Sort(items)
return &yamlmeta.Map{Items: items}

case *ScalarType:
var items openAPIKeys
items = append(items, collectDocumentation(typedValue)...)
items = append(items, o.collectDocumentation(typedValue)...)
items = append(items, o.convertValidations(typedValue)...)
items = append(items, &yamlmeta.MapItem{Key: defaultProp, Value: typedValue.GetDefaultValue()})

typeString := o.openAPITypeFor(typedValue)
items = append(items, &yamlmeta.MapItem{Key: typeProp, Value: typeString})

items = append(items, convertValidations(typedValue.GetValidationMap())...)

if typedValue.String() == "float" {
items = append(items, &yamlmeta.MapItem{Key: formatProp, Value: "float"})
}

sort.Sort(items)
return &yamlmeta.Map{Items: items}

case *NullType:
var items openAPIKeys
items = append(items, collectDocumentation(typedValue)...)
items = append(items, o.collectDocumentation(typedValue)...)
items = append(items, o.convertValidations(typedValue)...)
items = append(items, &yamlmeta.MapItem{Key: nullableProp, Value: true})

properties := o.calculateProperties(typedValue.GetValueType())
items = append(items, properties.Items...)

sort.Sort(items)
return &yamlmeta.Map{Items: items}

case *AnyType:
var items openAPIKeys
items = append(items, collectDocumentation(typedValue)...)
items = append(items, o.collectDocumentation(typedValue)...)
items = append(items, o.convertValidations(typedValue)...)
items = append(items, &yamlmeta.MapItem{Key: nullableProp, Value: true})
items = append(items, &yamlmeta.MapItem{Key: defaultProp, Value: typedValue.GetDefaultValue()})

sort.Sort(items)
return &yamlmeta.Map{Items: items}

default:
panic(fmt.Sprintf("Unrecognized type %T", schemaVal))
}
}

func collectDocumentation(typedValue Type) []*yamlmeta.MapItem {
func (*OpenAPIDocument) collectDocumentation(typedValue Type) []*yamlmeta.MapItem {
var items []*yamlmeta.MapItem
if typedValue.GetTitle() != "" {
items = append(items, &yamlmeta.MapItem{Key: titleProp, Value: typedValue.GetTitle()})
Expand All @@ -185,26 +205,53 @@ func collectDocumentation(typedValue Type) []*yamlmeta.MapItem {
}

// convertValidations converts the starlark validation map to a list of OpenAPI properties
func convertValidations(validations map[string]interface{}) []*yamlmeta.MapItem {
func (*OpenAPIDocument) convertValidations(schemaVal Type) []*yamlmeta.MapItem {
validation := schemaVal.GetValidation()
if validation == nil {
return nil
}

var items []*yamlmeta.MapItem
for key, value := range validations {
switch key {
case "min":
items = append(items, &yamlmeta.MapItem{Key: minProp, Value: value})
case "max":
items = append(items, &yamlmeta.MapItem{Key: maxProp, Value: value})
case "minLength":
items = append(items, &yamlmeta.MapItem{Key: minLenProp, Value: value})
case "maxLength":
items = append(items, &yamlmeta.MapItem{Key: maxLenProp, Value: value})
case "oneOf":
items = append(items, &yamlmeta.MapItem{Key: enumProp, Value: value})

if value, found := validation.HasSimpleMinLength(); found {
containedValue := schemaVal.GetValueType()
switch containedValue.(type) {
case *ArrayType:
items = append(items, &yamlmeta.MapItem{Key: minItemsProp, Value: value})
case *MapType:
items = append(items, &yamlmeta.MapItem{Key: minPropertiesProp, Value: value})
default:
if containedValue.String() == "string" {
items = append(items, &yamlmeta.MapItem{Key: minLenProp, Value: value})
}
}
}
if value, found := validation.HasSimpleMaxLength(); found {
containedValue := schemaVal.GetValueType()
switch containedValue.(type) {
case *ArrayType:
items = append(items, &yamlmeta.MapItem{Key: maxItemsProp, Value: value})
case *MapType:
items = append(items, &yamlmeta.MapItem{Key: maxPropertiesProp, Value: value})
default:
if containedValue.String() == "string" {
items = append(items, &yamlmeta.MapItem{Key: maxLenProp, Value: value})
}
}
}
if value, found := validation.HasSimpleMin(); found {
items = append(items, &yamlmeta.MapItem{Key: minProp, Value: value})
}
if value, found := validation.HasSimpleMax(); found {
items = append(items, &yamlmeta.MapItem{Key: maxProp, Value: value})
}
if value, found := validation.HasSimpleOneOf(); found {
items = append(items, &yamlmeta.MapItem{Key: enumProp, Value: value})
}
return items
}

func (o *OpenAPIDocument) openAPITypeFor(astType *ScalarType) string {
func (*OpenAPIDocument) openAPITypeFor(astType *ScalarType) string {
switch astType.ValueType {
case StringType:
return "string"
Expand Down
Loading

0 comments on commit 2e686ef

Please sign in to comment.