encoding/openapi: add FieldFilter option

This option allows excluding certain fields from the output.

The exclusion happens through a single regexp,
where a field name is qualified by its object type
and name.

Change-Id: I60913d92eec8ad60da73904e016c75fc61dcce98
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2444
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index 8224a71..9adfbe6 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -15,16 +15,17 @@
 package openapi
 
 import (
-	"fmt"
 	"math"
 	"math/big"
 	"path"
+	"regexp"
 	"sort"
 	"strconv"
 	"strings"
 
 	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/token"
 	"github.com/cockroachdb/apd/v2"
 )
 
@@ -33,9 +34,10 @@
 	refPrefix string
 	path      []string
 
-	expandRefs bool
-	nameFunc   func(inst *cue.Instance, path []string) string
-	descFunc   func(v cue.Value) string
+	expandRefs  bool
+	nameFunc    func(inst *cue.Instance, path []string) string
+	descFunc    func(v cue.Value) string
+	fieldFilter *regexp.Regexp
 
 	schemas *OrderedMap
 
@@ -55,6 +57,23 @@
 type typeFunc func(b *builder, a cue.Value)
 
 func schemas(g *Generator, inst *cue.Instance) (schemas OrderedMap, err error) {
+	var fieldFilter *regexp.Regexp
+	if g.FieldFilter != "" {
+		fieldFilter, err = regexp.Compile(g.FieldFilter)
+		if err != nil {
+			return nil, errors.Newf(token.NoPos, "invalid field filter: %v", err)
+		}
+
+		// verify that certain elements are still passed.
+		for _, f := range strings.Split(
+			"version,title,allOf,anyOf,not,enum,Schema/properties,Schema/items"+
+				"nullable,type", ",") {
+			if fieldFilter.MatchString(f) {
+				return nil, errors.Newf(token.NoPos, "field filter may not exclude %q", f)
+			}
+		}
+	}
+
 	c := buildContext{
 		inst:         inst,
 		refPrefix:    "components/schema",
@@ -63,6 +82,7 @@
 		descFunc:     g.DescriptionFunc,
 		schemas:      &OrderedMap{},
 		externalRefs: map[string]*externalType{},
+		fieldFilter:  fieldFilter,
 	}
 
 	defer func() {
@@ -73,10 +93,6 @@
 		default:
 			panic(x)
 		}
-		if x := recover(); x != nil {
-			path := strings.Join(c.path, ".")
-			err = fmt.Errorf("error: %s: %v", path, x)
-		}
 	}()
 
 	// Although paths is empty for now, it makes it valid OpenAPI spec.
@@ -247,7 +263,7 @@
 			}
 			fallthrough
 		default:
-			b.set("default", v)
+			b.setFilter("Schema", "default", v)
 		}
 	}
 	return isRef
@@ -440,7 +456,7 @@
 		required = append(required, i.Label())
 	}
 	if len(required) > 0 {
-		b.set("required", required)
+		b.setFilter("Schema", "required", required)
 	}
 
 	properties := map[string]*oaSchema{}
@@ -452,7 +468,7 @@
 	}
 
 	if t, ok := v.Elem(); ok {
-		b.set("additionalProperties", b.schema("*", t))
+		b.setFilter("Schema", "additionalProperties", b.schema("*", t))
 	}
 
 	// TODO: maxProperties, minProperties: can be done once we allow cap to
@@ -519,7 +535,7 @@
 		if typ, ok := v.Elem(); ok {
 			t := b.schema("*", typ)
 			if len(items) > 0 {
-				b.set("additionalItems", t)
+				b.setFilter("Schema", "additionalItems", t)
 			} else {
 				b.set("items", t)
 			}
@@ -530,14 +546,14 @@
 func (b *builder) listCap(v cue.Value) {
 	switch op, a := v.Expr(); op {
 	case cue.LessThanOp:
-		b.set("maxItems", b.int(a[0])-1)
+		b.setFilter("Schema", "maxItems", b.int(a[0])-1)
 	case cue.LessThanEqualOp:
-		b.set("maxItems", b.int(a[0]))
+		b.setFilter("Schema", "maxItems", b.int(a[0]))
 	case cue.GreaterThanOp:
-		b.set("minItems", b.int(a[0])+1)
+		b.setFilter("Schema", "minItems", b.int(a[0])+1)
 	case cue.GreaterThanEqualOp:
 		if b.int(a[0]) > 0 {
-			b.set("minItems", b.int(a[0]))
+			b.setFilter("Schema", "minItems", b.int(a[0]))
 		}
 	case cue.NoOp:
 		// must be type, so okay.
@@ -557,10 +573,6 @@
 func (b *builder) number(v cue.Value) {
 	// Multiple conjuncts mostly means just additive constraints.
 	// Type may be number of float.
-	// TODO: deterimine integer kind.
-	// if v.IsInt() {
-	// 	b.typ = "integer"
-	// }
 
 	switch op, a := v.Expr(); op {
 	// TODO: support the following JSON schema constraints
@@ -568,16 +580,16 @@
 	// setIntConstraint(t, "multipleOf", a)
 
 	case cue.LessThanOp:
-		b.set("exclusiveMaximum", b.big(a[0]))
+		b.setFilter("Schema", "exclusiveMaximum", b.big(a[0]))
 
 	case cue.LessThanEqualOp:
-		b.set("maximum", b.big(a[0]))
+		b.setFilter("Schema", "maximum", b.big(a[0]))
 
 	case cue.GreaterThanOp:
-		b.set("exclusiveMinimum", b.big(a[0]))
+		b.setFilter("Schema", "exclusiveMinimum", b.big(a[0]))
 
 	case cue.GreaterThanEqualOp:
-		b.set("minimum", b.big(a[0]))
+		b.setFilter("Schema", "minimum", b.big(a[0]))
 
 	case cue.NotEqualOp:
 		i := b.big(a[0])
@@ -646,7 +658,7 @@
 			return
 		}
 		if op == cue.RegexMatchOp {
-			b.set("pattern", s)
+			b.setFilter("schema", "pattern", s)
 		} else {
 			b.setNot("pattern", s)
 		}
@@ -677,7 +689,7 @@
 		}
 
 		if op == cue.RegexMatchOp {
-			b.set("pattern", s)
+			b.setFilter("Schema", "pattern", s)
 		} else {
 			b.setNot("pattern", s)
 		}
@@ -733,6 +745,14 @@
 	}
 }
 
+// setFilter is like set, but allows the key-value pair to be filtered.
+func (b *builder) setFilter(schema, key string, v interface{}) {
+	if re := b.ctx.fieldFilter; re != nil && re.MatchString(path.Join(schema, key)) {
+		return
+	}
+	b.set(key, v)
+}
+
 func (b *builder) set(key string, v interface{}) {
 	if b.current == nil {
 		b.current = &OrderedMap{}
diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go
index 93a4036..58cb41f 100644
--- a/encoding/openapi/openapi.go
+++ b/encoding/openapi/openapi.go
@@ -40,6 +40,14 @@
 	// in this document.
 	SelfContained bool
 
+	// 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
+	// an error for such fields to be excluded by this filter.
+	// Fields are qualified by their Object type. For instance, the
+	// minimum field of the schema object is qualified as Schema/minimum.
+	FieldFilter string
+
 	// ExpandReferences replaces references with actual objects when generating
 	// OpenAPI Schema. It is an error for an CUE value to refer to itself.
 	ExpandReferences bool
diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go
index 6e41e2d..7bb1b0c 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -43,6 +43,10 @@
 		"simple.json",
 		resolveRefs,
 	}, {
+		"simple.cue",
+		"simple-filter.json",
+		&Config{Info: info, FieldFilter: "min.*|max.*"},
+	}, {
 		"array.cue",
 		"array.json",
 		defaultConfig,
diff --git a/encoding/openapi/testdata/simple-filter.json b/encoding/openapi/testdata/simple-filter.json
new file mode 100644
index 0000000..a971c73
--- /dev/null
+++ b/encoding/openapi/testdata/simple-filter.json
@@ -0,0 +1,37 @@
+{
+   "openapi": "3.0.0",
+   "info": {
+      "title": "test",
+      "version": "v1"
+   },
+   "components": {
+      "schema": {
+         "MyStruct": {
+            "type": "object",
+            "required": [
+               "mediumNum",
+               "smallNum",
+               "float",
+               "double"
+            ],
+            "properties": {
+               "double": {
+                  "type": "number",
+                  "format": "double"
+               },
+               "float": {
+                  "type": "number",
+                  "format": "float"
+               },
+               "mediumNum": {
+                  "type": "integer",
+                  "format": "int32"
+               },
+               "smallNum": {
+                  "type": "integer"
+               }
+            }
+         }
+      }
+   }
+}
\ No newline at end of file