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

Export named validations to openApi properties #899

Merged
merged 2 commits into from
Apr 10, 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
92 changes: 88 additions & 4 deletions pkg/cmd/template/schema_inspect_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,6 +541,90 @@ components:

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

schemaYAML := `#@data/values-schema
---
foo:
#@schema/default 10
#@schema/validation min=0, max=100
range_key: 0

#@schema/default 10
#@schema/validation min=0
min_key: 0

#@schema/default 10
#@schema/validation max=100
max_key: 0

#@schema/validation min_len=1, max_len=10
string_key: ""

#@schema/validation one_of=[1,2,3]
one_of_integers: 1

#@schema/validation one_of=["one", "two", "three"]
one_of_strings: "one"
`
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: 10
minimum: 0
maximum: 100
min_key:
type: integer
default: 10
minimum: 0
max_key:
type: integer
default: 10
maximum: 100
string_key:
type: string
default: ""
minLength: 1
maxLength: 10
one_of_integers:
type: integer
default: 1
enum:
- 1
- 2
- 3
one_of_strings:
type: string
default: one
enum:
- one
- two
- three
`

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) {
Expand All @@ -559,8 +643,8 @@ db_conn:
#@schema/default "host"
#@schema/deprecated ""
hostname: ""
#@schema/title "Port Title"
#@schema/desc "Port should be float between 0.152 through 16.35"
#@schema/title "Port Title"
#@schema/desc "Port should be float between 0.152 through 16.35"
#@schema/nullable
#@schema/examples ("", 1.5)
#@schema/default 9.9
Expand Down Expand Up @@ -619,7 +703,7 @@ components:
#@schema/desc "List of database connections"
db_conn:
#@schema/desc "A network entry"
-
-
#@schema/desc "The hostname"
hostname: ""
#@schema/desc "Port should be between 49152 through 65535"
Expand Down Expand Up @@ -770,7 +854,7 @@ components:
#@schema/examples ("db_conn example description", [{"hostname": "localhost", "port": 8080, "timeout": 4.2, "any_key": "anything", "null_key": None}])
db_conn:
#@schema/examples ("db_conn array example description", {"hostname": "localhost", "port": 8080, "timeout": 4.2, "any_key": "anything", "null_key": "not null"})
-
-
#@schema/examples ("hostname example description", "localhost")
#@schema/desc "The hostname"
hostname: ""
Expand Down
33 changes: 33 additions & 0 deletions pkg/schema/openapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ const (
itemsProp = "items"
propertiesProp = "properties"
defaultProp = "default"
minProp = "minimum"
maxProp = "maximum"
minLenProp = "minLength"
maxLenProp = "maxLength"
enumProp = "enum"
)

var propOrder = map[string]int{
Expand All @@ -39,6 +44,11 @@ var propOrder = map[string]int{
itemsProp: 9,
propertiesProp: 10,
defaultProp: 11,
minProp: 12,
maxProp: 13,
minLenProp: 14,
maxLenProp: 15,
enumProp: 16,
}

type openAPIKeys []*yamlmeta.MapItem
Expand Down Expand Up @@ -123,6 +133,9 @@ func (o *OpenAPIDocument) calculateProperties(schemaVal interface{}) *yamlmeta.M

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"})
}
Expand Down Expand Up @@ -171,6 +184,26 @@ func collectDocumentation(typedValue Type) []*yamlmeta.MapItem {
return items
}

// convertValidations converts the starlark validation map to a list of OpenAPI properties
func convertValidations(validations map[string]interface{}) []*yamlmeta.MapItem {
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})
}
}
return items
}

func (o *OpenAPIDocument) openAPITypeFor(astType *ScalarType) string {
switch astType.ValueType {
case StringType:
Expand Down
54 changes: 54 additions & 0 deletions pkg/schema/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Type interface {
IsDeprecated() (bool, string)
SetDeprecated(bool, string)
GetValidation() *validations.NodeValidation
GetValidationMap() map[string]interface{}
String() string
}

Expand Down Expand Up @@ -83,6 +84,7 @@ type ScalarType struct {
Position *filepos.Position
defaultValue interface{}
documentation documentation
validations map[string]interface{}
}

type AnyType struct {
Expand Down Expand Up @@ -117,6 +119,9 @@ func (m MapType) GetValueType() Type {

// GetValueType provides the type of the value
func (t MapItemType) GetValueType() Type {
if _, ok := t.ValueType.(*ScalarType); ok && t.validations != nil {
t.ValueType.(*ScalarType).validations = t.GetValidationMap()
}
return t.ValueType
}

Expand Down Expand Up @@ -616,6 +621,55 @@ func (n NullType) GetValidation() *validations.NodeValidation {
return nil
}

// GetValidationMap provides the OpenAPI validation for the type
func (t *DocumentType) GetValidationMap() map[string]interface{} {
if t.validations != nil {
return t.validations.ValidationMap()
}
return nil
}

// GetValidationMap provides the OpenAPI validation for the type
func (m MapType) GetValidationMap() map[string]interface{} {
panic("Not implemented because MapType doesn't support validations")
}

// GetValidationMap provides the OpenAPI validation for the type
func (t MapItemType) GetValidationMap() map[string]interface{} {
if t.validations != nil {
return t.validations.ValidationMap()
}
return nil
}

// GetValidationMap provides the OpenAPI validation for the type
func (a ArrayType) GetValidationMap() map[string]interface{} {
panic("Not implemented because ArrayType doesn't support validations")
}

// GetValidationMap provides the OpenAPI validation for the type
func (a ArrayItemType) GetValidationMap() map[string]interface{} {
if a.validations != nil {
return a.validations.ValidationMap()
}
return nil
}

// GetValidationMap provides the OpenAPI validation for the type
func (s ScalarType) GetValidationMap() map[string]interface{} {
return s.validations
}

// GetValidationMap provides the OpenAPI validation for the type
func (a AnyType) GetValidationMap() map[string]interface{} {
panic("Not implemented because it is unreachable")
}

// GetValidationMap provides the OpenAPI validation for the type
func (n NullType) GetValidationMap() map[string]interface{} {
panic("Not implemented because it is unreachable")
}

// String produces a user-friendly name of the expected type.
func (t *DocumentType) String() string {
return yamlmeta.TypeName(&yamlmeta.Document{})
Expand Down
46 changes: 46 additions & 0 deletions pkg/validations/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"reflect"
"sort"
"strconv"
"strings"

"carvel.dev/ytt/pkg/filepos"
Expand Down Expand Up @@ -56,6 +57,51 @@ type validationKwargs struct {
oneOf starlark.Sequence
}

// ValidationMap returns a map of the validationKwargs and their values.
func (v NodeValidation) ValidationMap() map[string]interface{} {
validations := make(map[string]interface{})

if v.kwargs.minLength != nil {
value, _ := v.kwargs.minLength.Int64()
validations["minLength"] = value
}
if v.kwargs.maxLength != nil {
value, _ := v.kwargs.maxLength.Int64()
validations["maxLength"] = value
}
if v.kwargs.min != nil {
value, _ := strconv.Atoi(v.kwargs.min.String())
validations["min"] = value
}
if v.kwargs.max != nil {
value, _ := strconv.Atoi(v.kwargs.max.String())
validations["max"] = value
}
if v.kwargs.oneOf != nil {
enum := []interface{}{}
iter := starlark.Iterate(v.kwargs.oneOf)
defer iter.Done()
var x starlark.Value
for iter.Next(&x) {
var val interface{}
switch x.Type() {
case "string":
val, _ = strconv.Unquote(x.String())
case "int":
val, _ = strconv.Atoi(x.String())
default:
val = x.String()
}

enum = append(enum, val)
}

validations["oneOf"] = enum
}

return validations
}

// Run takes a root Node, and threadName, and validates each Node in the tree.
//
// When a Node's value is invalid, the errors are collected and returned in a Check.
Expand Down
Loading