diff --git a/pkg/cmd/template/schema_inspect_test.go b/pkg/cmd/template/schema_inspect_test.go index 86259093..2e89d538 100644 --- a/pkg/cmd/template/schema_inspect_test.go +++ b/pkg/cmd/template/schema_inspect_test.go @@ -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 @@ -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: @@ -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 @@ -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 @@ -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{ @@ -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) { diff --git a/pkg/schema/openapi.go b/pkg/schema/openapi.go index 69d69900..e92cb071 100644 --- a/pkg/schema/openapi.go +++ b/pkg/schema/openapi.go @@ -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" ) @@ -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 { @@ -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()}) @@ -126,25 +141,27 @@ 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()) @@ -152,20 +169,23 @@ func (o *OpenAPIDocument) calculateProperties(schemaVal interface{}) *yamlmeta.M 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()}) @@ -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" diff --git a/pkg/schema/type.go b/pkg/schema/type.go index 45a58c60..4ef273cf 100644 --- a/pkg/schema/type.go +++ b/pkg/schema/type.go @@ -30,7 +30,6 @@ type Type interface { IsDeprecated() (bool, string) SetDeprecated(bool, string) GetValidation() *validations.NodeValidation - GetValidationMap() map[string]interface{} String() string } @@ -84,7 +83,6 @@ type ScalarType struct { Position *filepos.Position defaultValue interface{} documentation documentation - validations map[string]interface{} } type AnyType struct { @@ -119,9 +117,6 @@ 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 } @@ -621,55 +616,6 @@ 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 66197929..45d2c759 100644 --- a/pkg/validations/validate.go +++ b/pkg/validations/validate.go @@ -7,7 +7,6 @@ import ( "fmt" "reflect" "sort" - "strconv" "strings" "carvel.dev/ytt/pkg/filepos" @@ -57,51 +56,6 @@ 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. @@ -154,6 +108,90 @@ func (a *validationRun) VisitWithParent(value yamlmeta.Node, parent yamlmeta.Nod return nil } +// HasSimpleMinLength indicates presence of min length validation and its associated value. +// Returns false if validation is conditional (via when=). +func (v NodeValidation) HasSimpleMinLength() (int64, bool) { + if v.kwargs.when != nil { + return 0, false + } + if v.kwargs.minLength != nil { + value, ok := v.kwargs.minLength.Int64() + if ok { + return value, true + } + } + return 0, false +} + +// HasSimpleMaxLength indicates presence of max length validation and its associated value. +// Returns false if validation is conditional (via when=). +func (v NodeValidation) HasSimpleMaxLength() (int64, bool) { + if v.kwargs.when != nil { + return 0, false + } + if v.kwargs.maxLength != nil { + value, ok := v.kwargs.maxLength.Int64() + if ok { + return value, true + } + } + return 0, false +} + +// HasSimpleMin indicates presence of min validation and its associated value. +// Returns false if validation is conditional (via when=). +func (v NodeValidation) HasSimpleMin() (interface{}, bool) { + if v.kwargs.when != nil { + return nil, false + } + if v.kwargs.min != nil { + value, err := core.NewStarlarkValue(v.kwargs.min).AsGoValue() + if err == nil { + return value, true + } + } + return nil, false +} + +// HasSimpleMax indicates presence of max validation and its associated value. +// Returns false if validation is conditional (via when=). +func (v NodeValidation) HasSimpleMax() (interface{}, bool) { + if v.kwargs.when != nil { + return nil, false + } + if v.kwargs.max != nil { + value, err := core.NewStarlarkValue(v.kwargs.max).AsGoValue() + if err == nil { + return value, true + } + } + return nil, false +} + +// HasSimpleOneOf indicates presence of one-of validation and its allowed values. +// Returns false if validation is conditional (via when=). +func (v NodeValidation) HasSimpleOneOf() ([]interface{}, bool) { + if v.kwargs.when != nil { + return nil, false + } + if v.kwargs.oneOf != nil { + enum := []interface{}{} + iter := starlark.Iterate(v.kwargs.oneOf) + defer iter.Done() + var x starlark.Value + for iter.Next(&x) { + value, err := core.NewStarlarkValue(x).AsGoValue() + if err == nil { + enum = append(enum, value) + } else { + return nil, false + } + } + return enum, true + } + return nil, false +} + // Validate runs the assertions in the rules with the node's value as arguments IF // the ValidationKwargs conditional options pass. // diff --git a/pkg/yttlibrary/json.go b/pkg/yttlibrary/json.go index b8c2af4d..f4e2269f 100644 --- a/pkg/yttlibrary/json.go +++ b/pkg/yttlibrary/json.go @@ -37,7 +37,7 @@ func (b jsonModule) Encode(thread *starlark.Thread, f *starlark.Builtin, args st return starlark.None, fmt.Errorf("expected exactly one argument") } allowedKWArgs := map[string]struct{}{ - "indent": {}, + "indent": {}, "escape_html": {}, } if err := core.CheckArgNames(kwargs, allowedKWArgs); err != nil {