encoding/openapi: simplify based on user-defined format
For instance, don't use minimum and maximum for
int32 boundaries if the user indicated the field is an
int32.
Also move to apd instead of ints.
Issue #56
Change-Id: I472c3c2b1fd8622430595bd062cef3bbd53b62f1
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2376
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index 0107f2d..10f93ef 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -17,6 +17,7 @@
import (
"fmt"
"math"
+ "math/big"
"path"
"sort"
"strconv"
@@ -24,6 +25,7 @@
"cuelang.org/go/cue"
"cuelang.org/go/cue/errors"
+ "github.com/cockroachdb/apd/v2"
)
type buildContext struct {
@@ -143,6 +145,7 @@
defer func() { b.ctx.path = oldPath }()
c := newRootBuilder(b.ctx)
+ c.format = extractFormat(v)
isRef := c.value(v, nil)
schema := c.finish()
@@ -156,6 +159,9 @@
schema.Prepend("description", str)
}
}
+
+ simplify(c, schema)
+
return schema
}
@@ -383,6 +389,8 @@
b.setType("integer", "") // may be overridden to integer
b.number(v)
+ // TODO: for JSON schema, consider adding multipleOf: 1.
+
case cue.BytesKind:
// byte string byte base64 encoded characters
// binary string binary any sequence of octets
@@ -555,19 +563,19 @@
// setIntConstraint(t, "multipleOf", a)
case cue.LessThanOp:
- b.set("exclusiveMaximum", b.int(a[0]))
+ b.set("exclusiveMaximum", b.big(a[0]))
case cue.LessThanEqualOp:
- b.set("maximum", b.int(a[0]))
+ b.set("maximum", b.big(a[0]))
case cue.GreaterThanOp:
- b.set("exclusiveMinimum", b.int(a[0]))
+ b.set("exclusiveMinimum", b.big(a[0]))
case cue.GreaterThanEqualOp:
- b.set("minimum", b.int(a[0]))
+ b.set("minimum", b.big(a[0]))
case cue.NotEqualOp:
- i := b.int(a[0])
+ i := b.big(a[0])
b.setNot("allOff", []*oaSchema{
b.kv("minItems", i),
b.kv("maxItems", i),
@@ -705,7 +713,9 @@
func (b *builder) setType(t, format string) {
if b.typ == "" {
b.typ = t
- b.format = format
+ if format != "" {
+ b.format = format
+ }
}
}
@@ -816,3 +826,13 @@
}
return d
}
+
+func (b *builder) big(v cue.Value) interface{} {
+ var mant big.Int
+ exp, err := v.MantExp(&mant)
+ if err != nil {
+ b.failf(v, "value not a number: %v", err)
+ return nil
+ }
+ return &decimal{apd.NewWithBigInt(&mant, int32(exp))}
+}
diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go
index 33a15e6..e1fd2e3 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -37,6 +37,14 @@
in, out string
config *Config
}{{
+ "simple.cue",
+ "simple.json",
+ resolveRefs,
+ }, {
+ "array.cue",
+ "array.json",
+ defaultConfig,
+ }, {
"oneof.cue",
"oneof.json",
defaultConfig,
diff --git a/encoding/openapi/testdata/array.cue b/encoding/openapi/testdata/array.cue
new file mode 100644
index 0000000..3261f81
--- /dev/null
+++ b/encoding/openapi/testdata/array.cue
@@ -0,0 +1,19 @@
+Arrays: {
+ bar?: [...MyEnum]
+ foo?: [...MyStruct]
+}
+
+Arrays: {
+ bar?: [...MyEnum]
+ foo?: [...MyStruct]
+}
+
+// MyStruct
+MyStruct: {
+ a?: int
+ e?: [...MyEnum]
+ e?: [...MyEnum]
+}
+
+// MyEnum
+MyEnum: *"1" | "2" | "3"
diff --git a/encoding/openapi/testdata/array.json b/encoding/openapi/testdata/array.json
new file mode 100644
index 0000000..4b29400
--- /dev/null
+++ b/encoding/openapi/testdata/array.json
@@ -0,0 +1,74 @@
+{
+ "openapi": "3.0.0",
+ "components": {
+ "schema": {
+ "Arrays": {
+ "type": "object",
+ "properties": {
+ "bar": {
+ "type": "array",
+ "items": {
+ "enum": [
+ "1",
+ "2",
+ "3"
+ ],
+ "default": "1"
+ }
+ },
+ "foo": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "a": {
+ "type": "integer"
+ },
+ "e": {
+ "type": "array",
+ "items": {
+ "enum": [
+ "1",
+ "2",
+ "3"
+ ],
+ "default": "1"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "MyEnum": {
+ "description": "MyEnum",
+ "enum": [
+ "1",
+ "2",
+ "3"
+ ],
+ "default": "1"
+ },
+ "MyStruct": {
+ "description": "MyStruct",
+ "type": "object",
+ "properties": {
+ "a": {
+ "type": "integer"
+ },
+ "e": {
+ "type": "array",
+ "items": {
+ "enum": [
+ "1",
+ "2",
+ "3"
+ ],
+ "default": "1"
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/encoding/openapi/testdata/oneof.json b/encoding/openapi/testdata/oneof.json
index c526b28..ce39ad4 100644
--- a/encoding/openapi/testdata/oneof.json
+++ b/encoding/openapi/testdata/oneof.json
@@ -11,7 +11,8 @@
],
"properties": {
"exact": {
- "type": "string"
+ "type": "string",
+ "format": "string"
}
}
},
@@ -22,7 +23,8 @@
],
"properties": {
"regex": {
- "type": "string"
+ "type": "string",
+ "format": "string"
}
}
}
diff --git a/encoding/openapi/testdata/openapi-norefs.json b/encoding/openapi/testdata/openapi-norefs.json
index 0094bd8..00803cd 100644
--- a/encoding/openapi/testdata/openapi-norefs.json
+++ b/encoding/openapi/testdata/openapi-norefs.json
@@ -15,7 +15,8 @@
"bar": {
"type": "array",
"items": {
- "type": "string"
+ "type": "string",
+ "format": "string"
}
},
"foo": {
@@ -68,7 +69,8 @@
],
"properties": {
"b": {
- "type": "string"
+ "type": "string",
+ "format": "string"
}
}
}
@@ -96,8 +98,7 @@
},
"Int32": {
"type": "integer",
- "minimum": -2147483648,
- "maximum": 2147483647
+ "format": "int32"
},
"YourMessage": {
"oneOf": [
@@ -108,10 +109,12 @@
],
"properties": {
"a": {
- "type": "string"
+ "type": "string",
+ "format": "string"
},
"b": {
- "type": "string"
+ "type": "string",
+ "format": "string"
}
}
},
@@ -122,7 +125,8 @@
],
"properties": {
"a": {
- "type": "string"
+ "type": "string",
+ "format": "string"
},
"b": {
"type": "number"
@@ -233,7 +237,8 @@
],
"properties": {
"a": {
- "type": "string"
+ "type": "string",
+ "format": "string"
}
}
}
diff --git a/encoding/openapi/testdata/openapi.json b/encoding/openapi/testdata/openapi.json
index 7409a75..c0ef7db 100644
--- a/encoding/openapi/testdata/openapi.json
+++ b/encoding/openapi/testdata/openapi.json
@@ -15,7 +15,8 @@
"bar": {
"type": "array",
"items": {
- "type": "string"
+ "type": "string",
+ "format": "string"
}
},
"foo": {
@@ -61,7 +62,8 @@
],
"properties": {
"b": {
- "type": "string"
+ "type": "string",
+ "format": "string"
}
}
}
@@ -89,8 +91,7 @@
},
"Int32": {
"type": "integer",
- "minimum": -2147483648,
- "maximum": 2147483647
+ "format": "int32"
},
"YourMessage": {
"oneOf": [
@@ -101,10 +102,12 @@
],
"properties": {
"a": {
- "type": "string"
+ "type": "string",
+ "format": "string"
},
"b": {
- "type": "string"
+ "type": "string",
+ "format": "string"
}
}
},
@@ -115,7 +118,8 @@
],
"properties": {
"a": {
- "type": "string"
+ "type": "string",
+ "format": "string"
},
"b": {
"type": "number"
@@ -226,7 +230,8 @@
],
"properties": {
"a": {
- "type": "string"
+ "type": "string",
+ "format": "string"
}
}
}
diff --git a/encoding/openapi/types.go b/encoding/openapi/types.go
new file mode 100644
index 0000000..03eada8
--- /dev/null
+++ b/encoding/openapi/types.go
@@ -0,0 +1,122 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package openapi
+
+import (
+ "github.com/cockroachdb/apd/v2"
+
+ "cuelang.org/go/cue"
+ "cuelang.org/go/cue/format"
+)
+
+// See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#format
+var cueToOpenAPI = map[string]string{
+ "int32": "int32",
+ "int64": "int64",
+
+ "float64": "double",
+ "float32": "float",
+
+ "string": "string",
+ "bytes": "binary",
+
+ // TODO: date, date-time, password.
+}
+
+func extractFormat(v cue.Value) string {
+ switch k := v.IncompleteKind(); {
+ case k&cue.NumberKind != 0, k&cue.StringKind != 0, k&cue.BytesKind != 0:
+ default:
+ return ""
+ }
+ b, err := format.Node(v.Syntax())
+ if err != nil {
+ return ""
+ }
+ return cueToOpenAPI[string(b)]
+}
+
+func simplify(b *builder, t *orderedMap) {
+ if b.format == "" {
+ return
+ }
+ switch b.typ {
+ case "number", "integer":
+ simplifyNumber(t, b.format)
+ }
+}
+
+func simplifyNumber(t *orderedMap, format string) string {
+ pairs := *t
+ k := 0
+ for i, kv := range pairs {
+ switch kv.key {
+ case "minimum":
+ if decimalEqual(minMap[format], kv.value) {
+ continue
+ }
+ case "maximum":
+ if decimalEqual(maxMap[format], kv.value) {
+ continue
+ }
+ }
+ pairs[i] = pairs[k]
+ k++
+ }
+ *t = pairs[:k]
+ return format
+}
+
+func decimalEqual(d *decimal, v interface{}) bool {
+ if d == nil {
+ return false
+ }
+ b, ok := v.(*decimal)
+ if !ok {
+ return false
+ }
+ return d.Cmp(b.Decimal) == 0
+}
+
+type decimal struct {
+ *apd.Decimal
+}
+
+func (d *decimal) MarshalJSON() (b []byte, err error) {
+ return d.MarshalText()
+}
+
+func mustDecimal(s string) *decimal {
+ d, _, err := apd.NewFromString(s)
+ if err != nil {
+ panic(err)
+ }
+ return &decimal{d}
+}
+
+var (
+ minMap = map[string]*decimal{
+ "int32": mustDecimal("-2147483648"),
+ "int64": mustDecimal("-9223372036854775808"),
+ "float": mustDecimal("-3.40282346638528859811704183484516925440e+38"),
+ "double": mustDecimal("-1.797693134862315708145274237317043567981e+308"),
+ }
+ maxMap = map[string]*decimal{
+ "int32": mustDecimal("2147483647"),
+ "int64": mustDecimal("9223372036854775807"),
+ "float": mustDecimal("+3.40282346638528859811704183484516925440e+38"),
+ "double": mustDecimal("+1.797693134862315708145274237317043567981e+308"),
+ }
+)