encoding/openapi: add option to close over imported types

To suppor this, two options are included:

SelfContained: to enable the feature.

NameFunc to allow user-defined fully qualified names
to disambiguate imported types.

Issue #56

Change-Id: I372f35c5fd71d204a60c8c26d4efce21f9457a27
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2375
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index ff4c6a5..0107f2d 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -18,6 +18,7 @@
 	"fmt"
 	"math"
 	"path"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -26,12 +27,24 @@
 )
 
 type buildContext struct {
+	inst      *cue.Instance
 	refPrefix string
 	path      []string
 
 	expandRefs bool
+	nameFunc   func(inst *cue.Instance, path []string) string
 
 	schemas *orderedMap
+
+	// Track external schemas.
+	externalRefs map[string]*externalType
+}
+
+type externalType struct {
+	ref   string
+	inst  *cue.Instance
+	path  []string
+	value cue.Value
 }
 
 type oaSchema = orderedMap
@@ -40,9 +53,12 @@
 
 func components(inst *cue.Instance, cfg *Config) (comps *orderedMap, err error) {
 	c := buildContext{
-		refPrefix:  "components/schema",
-		expandRefs: cfg.ExpandReferences,
-		schemas:    &orderedMap{},
+		inst:         inst,
+		refPrefix:    "components/schema",
+		expandRefs:   cfg.ExpandReferences,
+		nameFunc:     cfg.ReferenceFunc,
+		schemas:      &orderedMap{},
+		externalRefs: map[string]*externalType{},
 	}
 
 	defer func() {
@@ -75,7 +91,24 @@
 		if c.isInternal(label) {
 			continue
 		}
-		c.schemas.Set(label, c.build(label, i.Value()))
+		c.schemas.Set(c.makeRef(inst, []string{label}), c.build(label, i.Value()))
+	}
+
+	// keep looping until a fixed point is reached.
+	for done := 0; len(c.externalRefs) != done; {
+		done = len(c.externalRefs)
+
+		// From now on, all references need to be expanded
+		external := []string{}
+		for k := range c.externalRefs {
+			external = append(external, k)
+		}
+		sort.Strings(external)
+
+		for _, k := range external {
+			ext := c.externalRefs[k]
+			c.schemas.Set(ext.ref, c.build(ext.ref, ext.value.Eval()))
+		}
 	}
 	return comps, nil
 }
@@ -110,20 +143,23 @@
 	defer func() { b.ctx.path = oldPath }()
 
 	c := newRootBuilder(b.ctx)
-	c.value(v, nil)
+	isRef := c.value(v, nil)
 	schema := c.finish()
-	doc := []string{}
-	for _, d := range v.Doc() {
-		doc = append(doc, d.Text())
-	}
-	if len(doc) > 0 {
-		str := strings.TrimSpace(strings.Join(doc, "\n"))
-		schema.Prepend("description", str)
+
+	if !isRef {
+		doc := []string{}
+		for _, d := range v.Doc() {
+			doc = append(doc, d.Text())
+		}
+		if len(doc) > 0 {
+			str := strings.TrimSpace(strings.Join(doc, "\n"))
+			schema.Prepend("description", str)
+		}
 	}
 	return schema
 }
 
-func (b *builder) value(v cue.Value, f typeFunc) {
+func (b *builder) value(v cue.Value, f typeFunc) (isRef bool) {
 	count := 0
 	disallowDefault := false
 	var values cue.Value
@@ -132,18 +168,28 @@
 		values = v
 		count = 1
 	} else {
+		dedup := map[string]bool{}
+		hasNoRef := false
 		for _, v := range appendSplit(nil, cue.AndOp, v) {
 			// This may be a reference to an enum. So we need to check references before
 			// dissecting them.
 			switch p, r := v.Reference(); {
 			case len(r) > 0:
+				ref := b.ctx.makeRef(p, r)
+				if dedup[ref] {
+					continue
+				}
+				dedup[ref] = true
+
 				b.addRef(v, p, r)
 				disallowDefault = true
 			default:
+				hasNoRef = true
 				count++
 				values = values.Unify(v)
 			}
 		}
+		isRef = !hasNoRef && len(dedup) == 1
 	}
 
 	if count > 0 { // TODO: implement IsAny.
@@ -193,6 +239,7 @@
 			b.set("default", v)
 		}
 	}
+	return isRef
 }
 
 func appendSplit(a []cue.Value, splitBy cue.Op, v cue.Value) []cue.Value {
@@ -729,10 +776,29 @@
 }
 
 func (b *builder) addRef(v cue.Value, inst *cue.Instance, ref []string) {
+	name := b.ctx.makeRef(inst, ref)
 	b.addConjunct(func(b *builder) {
-		a := append([]string{"#", b.ctx.refPrefix}, ref...)
-		b.set("$ref", path.Join(a...))
+		b.set("$ref", path.Join("#", b.ctx.refPrefix, name))
 	})
+
+	if b.ctx.inst != inst {
+		b.ctx.externalRefs[name] = &externalType{
+			ref:   name,
+			inst:  inst,
+			path:  ref,
+			value: v,
+		}
+	}
+}
+
+func (b *buildContext) makeRef(inst *cue.Instance, ref []string) string {
+	a := make([]string, 0, len(ref)+3)
+	if b.nameFunc != nil {
+		a = append(a, b.nameFunc(inst, ref))
+	} else {
+		a = append(a, ref...)
+	}
+	return path.Join(a...)
 }
 
 func (b *builder) int(v cue.Value) int64 {
diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go
index 9e3acd2..b1a4628 100644
--- a/encoding/openapi/openapi.go
+++ b/encoding/openapi/openapi.go
@@ -22,6 +22,14 @@
 
 // A Config defines options for mapping CUE to and from OpenAPI.
 type Config struct {
+	// ReferenceFunc allows users to specify an alternative representation
+	// for references.
+	ReferenceFunc func(inst *cue.Instance, path []string) string
+
+	// SelfContained causes all non-expanded external references to be included
+	// in this document.
+	SelfContained bool
+
 	// ExpandReferences replaces references with actual objects when generating
 	// OpenAPI Schema. It is an error for an CUE value to refer to itself
 	// when this object is used.
diff --git a/encoding/openapi/testdata/simple.cue b/encoding/openapi/testdata/simple.cue
new file mode 100644
index 0000000..0504d4c
--- /dev/null
+++ b/encoding/openapi/testdata/simple.cue
@@ -0,0 +1,7 @@
+MyStruct: {
+	mediumNum: int32
+	smallNum:  int8
+
+	float:  float32
+	double: float64
+}
diff --git a/encoding/openapi/testdata/simple.json b/encoding/openapi/testdata/simple.json
new file mode 100644
index 0000000..8110d69
--- /dev/null
+++ b/encoding/openapi/testdata/simple.json
@@ -0,0 +1,35 @@
+{
+   "openapi": "3.0.0",
+   "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",
+                  "minimum": -128,
+                  "maximum": 127
+               }
+            }
+         }
+      }
+   }
+}
\ No newline at end of file