diff --git a/cmd/cue/cmd/testdata/script/def_openapi.txt b/cmd/cue/cmd/testdata/script/def_openapi.txt index 3e0593712..3ea4fbc4e 100644 --- a/cmd/cue/cmd/testdata/script/def_openapi.txt +++ b/cmd/cue/cmd/testdata/script/def_openapi.txt @@ -143,7 +143,8 @@ $version: "v1" "b": { "type": "integer", "minimum": 0, - "exclusiveMaximum": 10 + "maximum": 10, + "exclusiveMaximum": true } } } @@ -176,7 +177,8 @@ components: b: type: integer minimum: 0 - exclusiveMaximum: 10 + maximum: 10 + exclusiveMaximum: true -- expect-cue-out -- openapi: "3.0.0" info: { @@ -198,7 +200,8 @@ components: schemas: { b: { type: "integer" minimum: 0 - exclusiveMaximum: 10 + maximum: 10 + exclusiveMaximum: true } } } diff --git a/cmd/cue/cmd/testdata/script/import_auto.txt b/cmd/cue/cmd/testdata/script/import_auto.txt index 785d24264..4c555f2d7 100644 --- a/cmd/cue/cmd/testdata/script/import_auto.txt +++ b/cmd/cue/cmd/testdata/script/import_auto.txt @@ -13,7 +13,7 @@ info: { #Foo: { a: int - b: int & >=0 & <10 + b: int & <10 & >=0 ... } #Bar: { diff --git a/encoding/jsonschema/constraints.go b/encoding/jsonschema/constraints.go index bc45cdf7e..ef941e244 100644 --- a/encoding/jsonschema/constraints.go +++ b/encoding/jsonschema/constraints.go @@ -403,24 +403,38 @@ var constraints = []*constraint{ // Number constraints - p1("minimum", func(n cue.Value, s *state) { + p2("minimum", func(n cue.Value, s *state) { s.usedTypes |= cue.NumberKind - s.add(n, numType, &ast.UnaryExpr{Op: token.GEQ, X: s.number(n)}) + op := token.GEQ + if s.exclusiveMin { + op = token.GTR + } + s.add(n, numType, &ast.UnaryExpr{Op: op, X: s.number(n)}) }), p1("exclusiveMinimum", func(n cue.Value, s *state) { - // TODO: should we support Draft 4 booleans? + if n.Kind() == cue.BoolKind { + s.exclusiveMin = true + return + } s.usedTypes |= cue.NumberKind s.add(n, numType, &ast.UnaryExpr{Op: token.GTR, X: s.number(n)}) }), - p1("maximum", func(n cue.Value, s *state) { + p2("maximum", func(n cue.Value, s *state) { s.usedTypes |= cue.NumberKind - s.add(n, numType, &ast.UnaryExpr{Op: token.LEQ, X: s.number(n)}) + op := token.LEQ + if s.exclusiveMax { + op = token.LSS + } + s.add(n, numType, &ast.UnaryExpr{Op: op, X: s.number(n)}) }), p1("exclusiveMaximum", func(n cue.Value, s *state) { - // TODO: should we support Draft 4 booleans? + if n.Kind() == cue.BoolKind { + s.exclusiveMax = true + return + } s.usedTypes |= cue.NumberKind s.add(n, numType, &ast.UnaryExpr{Op: token.LSS, X: s.number(n)}) }), diff --git a/encoding/jsonschema/decode.go b/encoding/jsonschema/decode.go index de83b8ced..30b0dc7fc 100644 --- a/encoding/jsonschema/decode.go +++ b/encoding/jsonschema/decode.go @@ -327,13 +327,15 @@ type state struct { usedTypes cue.Kind allowedTypes cue.Kind - default_ ast.Expr - examples []ast.Expr - title string - description string - deprecated bool - jsonschema string - id *url.URL // base URI for $ref + default_ ast.Expr + examples []ast.Expr + title string + description string + deprecated bool + exclusiveMin bool // For OpenAPI and legacy support. + exclusiveMax bool // For OpenAPI and legacy support. + jsonschema string + id *url.URL // base URI for $ref definitions []ast.Decl diff --git a/encoding/jsonschema/testdata/num.txtar b/encoding/jsonschema/testdata/num.txtar index cbbd984e6..0b53a7a8f 100644 --- a/encoding/jsonschema/testdata/num.txtar +++ b/encoding/jsonschema/testdata/num.txtar @@ -23,6 +23,13 @@ "maximum": 3, "maxLength": 5 }, + "legacy": { + "type": "number", + "exclusiveMinimum": true, + "minimum": 2, + "exclusiveMaximum": true, + "maximum": 3 + }, "cents": { "type": "number", "multipleOf": 0.05 @@ -42,4 +49,5 @@ several?: 1 | 2 | 3 | 4 inclusive?: >=2 & <=3 exclusive?: int & >2 & <3 multi?: int & >=2 & <=3 | strings.MaxRunes(5) +legacy?: >2 & <3 cents?: math.MultipleOf(0.05) diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go index e3f5d17d8..adb966978 100644 --- a/encoding/openapi/build.go +++ b/encoding/openapi/build.go @@ -37,12 +37,13 @@ type buildContext struct { refPrefix string path []string - expandRefs bool - structural bool - nameFunc func(inst *cue.Instance, path []string) string - descFunc func(v cue.Value) string - fieldFilter *regexp.Regexp - evalDepth int // detect cycles when resolving references + expandRefs bool + structural bool + exclusiveBool bool + nameFunc func(inst *cue.Instance, path []string) string + descFunc func(v cue.Value) string + fieldFilter *regexp.Regexp + evalDepth int // detect cycles when resolving references schemas *OrderedMap @@ -79,6 +80,10 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro } } + if g.Version == "" { + g.Version = "3.0.0" + } + c := buildContext{ inst: inst, instExt: inst, @@ -92,6 +97,14 @@ func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err erro fieldFilter: fieldFilter, } + switch g.Version { + case "3.0.0": + c.exclusiveBool = true + case "3.1.0": + default: + return nil, errors.Newf(token.NoPos, "unsupported version %s", g.Version) + } + defer func() { switch x := recover().(type) { case nil: @@ -888,13 +901,23 @@ func (b *builder) number(v cue.Value) { switch op, a := v.Expr(); op { case cue.LessThanOp: - b.setFilter("Schema", "exclusiveMaximum", b.big(a[0])) + if b.ctx.exclusiveBool { + b.setFilter("Schema", "exclusiveMaximum", ast.NewBool(true)) + b.setFilter("Schema", "maximum", b.big(a[0])) + } else { + b.setFilter("Schema", "exclusiveMaximum", b.big(a[0])) + } case cue.LessThanEqualOp: b.setFilter("Schema", "maximum", b.big(a[0])) case cue.GreaterThanOp: - b.setFilter("Schema", "exclusiveMinimum", b.big(a[0])) + if b.ctx.exclusiveBool { + b.setFilter("Schema", "exclusiveMinimum", ast.NewBool(true)) + b.setFilter("Schema", "minimum", b.big(a[0])) + } else { + b.setFilter("Schema", "exclusiveMinimum", b.big(a[0])) + } case cue.GreaterThanEqualOp: b.setFilter("Schema", "minimum", b.big(a[0])) diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go index 96133315e..f26cff6e7 100644 --- a/encoding/openapi/openapi.go +++ b/encoding/openapi/openapi.go @@ -51,6 +51,9 @@ type Config struct { // in this document. SelfContained bool + // OpenAPI version to use. Supported as of v3.0.0. + Version string + // FieldFilter defines a regular expression of all fields to omit from the // output. It is only allowed to filter fields that add additional // constraints. Fields that indicate basic types cannot be removed. It is @@ -203,7 +206,7 @@ func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.Str } return ast.NewStruct( - "openapi", ast.NewString("3.0.0"), + "openapi", ast.NewString(c.Version), "info", info, "paths", ast.NewStruct(), "components", ast.NewStruct("schemas", schemas), diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go index d2f32e6a6..e8c37058e 100644 --- a/encoding/openapi/openapi_test.go +++ b/encoding/openapi/openapi_test.go @@ -77,6 +77,10 @@ func TestParseDefinitions(t *testing.T) { "nums.cue", "nums.json", defaultConfig, + }, { + "nums.cue", + "nums-v3.1.0.json", + &openapi.Config{Info: info, Version: "3.1.0"}, }, { "builtins.cue", "builtins.json", diff --git a/encoding/openapi/testdata/issue131.json b/encoding/openapi/testdata/issue131.json index 7257f5f4f..3025bf567 100644 --- a/encoding/openapi/testdata/issue131.json +++ b/encoding/openapi/testdata/issue131.json @@ -16,11 +16,13 @@ "properties": { "a": { "type": "number", - "exclusiveMinimum": 50 + "minimum": 50, + "exclusiveMinimum": true }, "b": { "type": "number", - "exclusiveMaximum": 10 + "maximum": 10, + "exclusiveMaximum": true } } }, diff --git a/encoding/openapi/testdata/nums-v3.1.0.json b/encoding/openapi/testdata/nums-v3.1.0.json new file mode 100644 index 000000000..4c3cc670f --- /dev/null +++ b/encoding/openapi/testdata/nums-v3.1.0.json @@ -0,0 +1,37 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "test", + "version": "v1" + }, + "paths": {}, + "components": { + "schemas": { + "exMax": { + "type": "number", + "exclusiveMaximum": 6 + }, + "exMin": { + "type": "number", + "exclusiveMinimum": 5 + }, + "mul": { + "type": "number", + "multipleOf": 5 + }, + "neq": { + "type": "number", + "not": { + "allOff": [ + { + "minimum": 4 + }, + { + "maximum": 4 + } + ] + } + } + } + } +} \ No newline at end of file diff --git a/encoding/openapi/testdata/nums.cue b/encoding/openapi/testdata/nums.cue index c765945e8..94d178b62 100644 --- a/encoding/openapi/testdata/nums.cue +++ b/encoding/openapi/testdata/nums.cue @@ -3,3 +3,6 @@ import "math" #mul: math.MultipleOf(5) #neq: !=4 + +#exMin: >5 +#exMax: <6 diff --git a/encoding/openapi/testdata/nums.json b/encoding/openapi/testdata/nums.json index c032f2040..18332db12 100644 --- a/encoding/openapi/testdata/nums.json +++ b/encoding/openapi/testdata/nums.json @@ -7,6 +7,16 @@ "paths": {}, "components": { "schemas": { + "exMax": { + "type": "number", + "maximum": 6, + "exclusiveMaximum": true + }, + "exMin": { + "type": "number", + "minimum": 5, + "exclusiveMinimum": true + }, "mul": { "type": "number", "multipleOf": 5 diff --git a/encoding/openapi/testdata/openapi-norefs.json b/encoding/openapi/testdata/openapi-norefs.json index 28397066c..f5c4dfd87 100644 --- a/encoding/openapi/testdata/openapi-norefs.json +++ b/encoding/openapi/testdata/openapi-norefs.json @@ -110,8 +110,10 @@ }, "foo": { "type": "number", - "exclusiveMinimum": 10, - "exclusiveMaximum": 1000 + "minimum": 10, + "exclusiveMinimum": true, + "maximum": 1000, + "exclusiveMaximum": true }, "bar": { "type": "array", diff --git a/encoding/openapi/testdata/openapi.json b/encoding/openapi/testdata/openapi.json index b34244d3b..90bd1f5ad 100644 --- a/encoding/openapi/testdata/openapi.json +++ b/encoding/openapi/testdata/openapi.json @@ -103,8 +103,10 @@ "$ref": "#/components/schemas/Int32" }, { - "exclusiveMinimum": 10, - "exclusiveMaximum": 1000 + "exclusiveMinimum": true, + "minimum": 10, + "exclusiveMaximum": true, + "maximum": 1000 } ] },