diff --git a/pkg/cmd/template/schema_inspect_test.go b/pkg/cmd/template/schema_inspect_test.go index 865fbd5c..bb2cc5c8 100644 --- a/pkg/cmd/template/schema_inspect_test.go +++ b/pkg/cmd/template/schema_inspect_test.go @@ -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) { @@ -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 @@ -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" @@ -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: "" diff --git a/pkg/schema/openapi.go b/pkg/schema/openapi.go index d80361b4..4644a3c8 100644 --- a/pkg/schema/openapi.go +++ b/pkg/schema/openapi.go @@ -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{ @@ -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 @@ -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"}) } @@ -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: diff --git a/pkg/schema/type.go b/pkg/schema/type.go index b41452f4..c670030c 100644 --- a/pkg/schema/type.go +++ b/pkg/schema/type.go @@ -30,6 +30,7 @@ type Type interface { IsDeprecated() (bool, string) SetDeprecated(bool, string) GetValidation() *validations.NodeValidation + GetValidationMap() map[string]interface{} String() string } @@ -83,6 +84,7 @@ type ScalarType struct { Position *filepos.Position defaultValue interface{} documentation documentation + validations map[string]interface{} } type AnyType struct { @@ -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 } @@ -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{}) diff --git a/pkg/validations/validate.go b/pkg/validations/validate.go index 061d73e2..2b09b8fb 100644 --- a/pkg/validations/validate.go +++ b/pkg/validations/validate.go @@ -7,6 +7,7 @@ import ( "fmt" "reflect" "sort" + "strconv" "strings" "carvel.dev/ytt/pkg/filepos" @@ -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.