encoding/openapi|jsonschema: allow bool for exclusiveNum

At least for v3.0.0 this is expected. Only
as of v3.1.0 will OpenAPI adopt the JSON schema
semantics.

Fixes #412

Change-Id: Ibb43ef4794ec6500e27392d981cda655ecec0517
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/6361
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cmd/cue/cmd/testdata/script/def_openapi.txt b/cmd/cue/cmd/testdata/script/def_openapi.txt
index 3e05937..3ea4fbc 100644
--- a/cmd/cue/cmd/testdata/script/def_openapi.txt
+++ b/cmd/cue/cmd/testdata/script/def_openapi.txt
@@ -143,7 +143,8 @@
                     "b": {
                         "type": "integer",
                         "minimum": 0,
-                        "exclusiveMaximum": 10
+                        "maximum": 10,
+                        "exclusiveMaximum": true
                     }
                 }
             }
@@ -176,7 +177,8 @@
         b:
           type: integer
           minimum: 0
-          exclusiveMaximum: 10
+          maximum: 10
+          exclusiveMaximum: true
 -- expect-cue-out --
 openapi: "3.0.0"
 info: {
@@ -198,7 +200,8 @@
 			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 785d242..4c555f2 100644
--- a/cmd/cue/cmd/testdata/script/import_auto.txt
+++ b/cmd/cue/cmd/testdata/script/import_auto.txt
@@ -13,7 +13,7 @@
 
 #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 bc45cdf..ef941e2 100644
--- a/encoding/jsonschema/constraints.go
+++ b/encoding/jsonschema/constraints.go
@@ -403,24 +403,38 @@
 
 	// 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 de83b8c..30b0dc7 100644
--- a/encoding/jsonschema/decode.go
+++ b/encoding/jsonschema/decode.go
@@ -327,13 +327,15 @@
 	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 cbbd984..0b53a7a 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 @@
 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 e3f5d17..adb9669 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -37,12 +37,13 @@
 	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 @@
 		}
 	}
 
+	if g.Version == "" {
+		g.Version = "3.0.0"
+	}
+
 	c := buildContext{
 		inst:         inst,
 		instExt:      inst,
@@ -92,6 +97,14 @@
 		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 @@
 
 	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 9613331..f26cff6 100644
--- a/encoding/openapi/openapi.go
+++ b/encoding/openapi/openapi.go
@@ -51,6 +51,9 @@
 	// 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 @@
 	}
 
 	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 d2f32e6..e8c3705 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -78,6 +78,10 @@
 		"nums.json",
 		defaultConfig,
 	}, {
+		"nums.cue",
+		"nums-v3.1.0.json",
+		&openapi.Config{Info: info, Version: "3.1.0"},
+	}, {
 		"builtins.cue",
 		"builtins.json",
 		defaultConfig,
diff --git a/encoding/openapi/testdata/issue131.json b/encoding/openapi/testdata/issue131.json
index 7257f5f..3025bf5 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 0000000..4c3cc67
--- /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 c765945..94d178b 100644
--- a/encoding/openapi/testdata/nums.cue
+++ b/encoding/openapi/testdata/nums.cue
@@ -3,3 +3,6 @@
 #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 c032f20..18332db 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 2839706..f5c4dfd 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 b34244d..90bd1f5 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
                      }
                   ]
                },