Skip to content

Commit

Permalink
Merge pull request #899 from peschmae/feat/openapi-validations
Browse files Browse the repository at this point in the history
Export named validations to openApi properties
  • Loading branch information
joaopapereira authored Apr 10, 2024
2 parents 4b40e30 + 7348f37 commit d652316
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 4 deletions.
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

0 comments on commit d652316

Please sign in to comment.