Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor openapi validation gen to avoid mutation inside GetValueType() #904

Merged
merged 4 commits into from
Apr 15, 2024
Merged
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
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
Loading