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"),
+	}
+)