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 {