encoding/openapi: allow empty string from ReferenceFunc

When users use ReferenceFunc, it may be hard to
map certain types, like CUE builtin types, to the custom
namespace. Returning "" now allows such references
to be expanded in place. In the case of CUE builtin types,
the represented type will be that of the underlying type
(usually a string).

Change-Id: I27b5bd749f2c5339a2e5381376333e77424ee682
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2760
Reviewed-by: Jason Wang <jasonwzm@google.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index bcb21d4..ba64a67 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -109,7 +109,11 @@
 		if c.isInternal(label) {
 			continue
 		}
-		c.schemas.Set(c.makeRef(inst, []string{label}), c.build(label, i.Value()))
+		ref := c.makeRef(inst, []string{label})
+		if ref == "" {
+			continue
+		}
+		c.schemas.Set(ref, c.build(label, i.Value()))
 	}
 
 	// keep looping until a fixed point is reached.
@@ -196,11 +200,21 @@
 	return schema
 }
 
-func resolve(v cue.Value) cue.Value {
+func (b *builder) resolve(v cue.Value) cue.Value {
+	// Cycles are not allowed when expanding references. Right now we just
+	// cap the depth of evaluation at 30.
+	// TODO: do something more principled.
+	const maxDepth = 30
+	if b.ctx.evalDepth > maxDepth {
+		b.failf(v, "maximum stack depth of %d reached", maxDepth)
+	}
+	b.ctx.evalDepth++
+	defer func() { b.ctx.evalDepth-- }()
+
 	switch op, a := v.Expr(); op {
 	case cue.SelectorOp:
 		field, _ := a[1].String()
-		v = resolve(a[0]).Lookup(field)
+		v = b.resolve(a[0]).Lookup(field)
 	}
 	return v
 }
@@ -210,17 +224,7 @@
 	disallowDefault := false
 	var values cue.Value
 	if b.ctx.expandRefs || b.format != "" {
-		// Cycles are not allowed when expanding references. Right now we just
-		// cap the depth of evaluation at 30.
-		// TODO: do something more principled.
-		const maxDepth = 30
-		if b.ctx.evalDepth > maxDepth {
-			b.failf(v, "maximum stack depth of %d reached", maxDepth)
-		}
-		b.ctx.evalDepth++
-		defer func() { b.ctx.evalDepth-- }()
-
-		values = resolve(v)
+		values = b.resolve(v)
 		count = 1
 	} else {
 		dedup := map[string]bool{}
@@ -231,6 +235,10 @@
 			switch p, r := v.Reference(); {
 			case len(r) > 0:
 				ref := b.ctx.makeRef(p, r)
+				if ref == "" {
+					v = b.resolve(v)
+					break
+				}
 				if dedup[ref] {
 					continue
 				}
@@ -238,11 +246,11 @@
 
 				b.addRef(v, p, r)
 				disallowDefault = true
-			default:
-				hasNoRef = true
-				count++
-				values = values.Unify(v)
+				continue
 			}
+			hasNoRef = true
+			count++
+			values = values.Unify(v)
 		}
 		isRef = !hasNoRef && len(dedup) == 1
 	}
@@ -502,7 +510,7 @@
 		// TODO: extract format from specific type.
 
 	default:
-		b.failf(v, "unsupported op %v for number type", op)
+		b.failf(v, "unsupported op %v for object type (%v)", op, v)
 		return
 	}
 
@@ -582,7 +590,7 @@
 		// TODO: extract format from specific type.
 
 	default:
-		b.failf(v, "unsupported op %v for number type", op)
+		b.failf(v, "unsupported op %v for array type", op)
 		return
 	}
 
@@ -698,7 +706,7 @@
 		// TODO: extract format from specific type.
 
 	default:
-		// panic(fmt.Sprintf("unsupported of %v for number type", op))
+		b.failf(v, "unsupported op for number %v", op)
 	}
 }
 
diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go
index 76ade5e..2622600 100644
--- a/encoding/openapi/openapi.go
+++ b/encoding/openapi/openapi.go
@@ -27,7 +27,8 @@
 	Info OrderedMap
 
 	// ReferenceFunc allows users to specify an alternative representation
-	// for references.
+	// for references. An empty string tells the generator to expand the type
+	// in place and not generate a schema for that entity, if applicable.
 	ReferenceFunc func(inst *cue.Instance, path []string) string
 
 	// DescriptionFunc allows rewriting a description associated with a certain
diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go
index bcdaf19..1459527 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -94,6 +94,19 @@
 				return "Randomly picked description from a set of size one."
 			},
 		},
+	}, {
+		"refs.cue",
+		"refs.json",
+		&Generator{
+			Info: info,
+			ReferenceFunc: func(inst *cue.Instance, path []string) string {
+				switch {
+				case strings.HasPrefix(path[0], "Excluded"):
+					return ""
+				}
+				return strings.Join(path, ".")
+			},
+		},
 	}}
 	for _, tc := range testCases {
 		t.Run(tc.out, func(t *testing.T) {
diff --git a/encoding/openapi/testdata/refs.cue b/encoding/openapi/testdata/refs.cue
new file mode 100644
index 0000000..2b11279
--- /dev/null
+++ b/encoding/openapi/testdata/refs.cue
@@ -0,0 +1,13 @@
+Keep: {
+	// This comment is included
+	excludedStruct: ExcludedStruct
+	excludedInt:    ExcludedInt
+}
+
+// ExcludedStruct is not included in the output.
+ExcludedStruct: {
+	A: int
+}
+
+// ExcludedInt is not included in the output.
+ExcludedInt: int
diff --git a/encoding/openapi/testdata/refs.json b/encoding/openapi/testdata/refs.json
new file mode 100644
index 0000000..c6d5654
--- /dev/null
+++ b/encoding/openapi/testdata/refs.json
@@ -0,0 +1,35 @@
+{
+   "openapi": "3.0.0",
+   "info": {
+      "title": "test",
+      "version": "v1"
+   },
+   "components": {
+      "schemas": {
+         "Keep": {
+            "type": "object",
+            "required": [
+               "excludedStruct",
+               "excludedInt"
+            ],
+            "properties": {
+               "excludedStruct": {
+                  "description": "This comment is included",
+                  "type": "object",
+                  "required": [
+                     "A"
+                  ],
+                  "properties": {
+                     "A": {
+                        "type": "integer"
+                     }
+                  }
+               },
+               "excludedInt": {
+                  "type": "integer"
+               }
+            }
+         }
+      }
+   }
+}
\ No newline at end of file