encoding/openapi: support nested defintitions

This perpares for Protobuf remodeling, where the nested
definitions of protobufs are are also nested within the
corresponding CUE types.

- Use Dereference
- Get comments from definitions, not fields, if applicable
- Allow embedded closed disjunctions.
- Allow nested types.
- Don't expand array element references.

Note that this removes the simplification in the openapi.cue
test: nested closed structs were not handled correctly by
this optimization.

Change-Id: Ie6adf44b37ac72139e21539094df5a728a309bd1
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5405
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/cue/value.go b/cue/value.go
index 52abfd1..026080b 100644
--- a/cue/value.go
+++ b/cue/value.go
@@ -1260,7 +1260,7 @@
 
 func appendDocComments(docs []*ast.CommentGroup, n ast.Node) []*ast.CommentGroup {
 	for _, c := range n.Comments() {
-		if c.Position == 0 {
+		if c.Doc {
 			docs = append(docs, c)
 		}
 	}
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index be55c39..8ceeb2e 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -33,6 +33,7 @@
 
 type buildContext struct {
 	inst      *cue.Instance
+	instExt   *cue.Instance
 	refPrefix string
 	path      []string
 
@@ -80,6 +81,7 @@
 
 	c := buildContext{
 		inst:         inst,
+		instExt:      inst,
 		refPrefix:    "components/schemas",
 		expandRefs:   g.ExpandReferences,
 		structural:   g.ExpandReferences,
@@ -132,7 +134,11 @@
 
 		for _, k := range external {
 			ext := c.externalRefs[k]
-			c.schemas.Set(ext.ref, c.build(ext.ref, ext.value.Eval()))
+			c.instExt = ext.inst
+			last := len(ext.path) - 1
+			c.path = ext.path[:last]
+			name := ext.path[last]
+			c.schemas.Set(ext.ref, c.build(name, cue.Dereference(ext.value)))
 		}
 	}
 
@@ -288,35 +294,12 @@
 	"default":          12,
 }
 
-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()
-		f, _ := b.resolve(a[0]).LookupField(field)
-		v = cue.Value{}
-		if !f.IsOptional {
-			v = f.Value
-		}
-	}
-	return v
-}
-
 func (b *builder) value(v cue.Value, f typeFunc) (isRef bool) {
 	count := 0
 	disallowDefault := false
 	var values cue.Value
 	if b.ctx.expandRefs || b.format != "" {
-		values = b.resolve(v)
+		values = cue.Dereference(v)
 		count = 1
 	} else {
 		dedup := map[string]bool{}
@@ -328,7 +311,7 @@
 			case len(r) > 0:
 				ref := b.ctx.makeRef(p, r)
 				if ref == "" {
-					v = b.resolve(v)
+					v = cue.Dereference(v)
 					break
 				}
 				if dedup[ref] {
@@ -370,27 +353,32 @@
 					}
 				}
 
-				if len(a) > 1 {
-					// Filter disjuncts that cannot unify with other conjuncts,
-					// and thus can never be satisfied.
-					// TODO: there should be generalized simplification logic
-					// in CUE (outside of the usual implicit simplifications).
-					k := 0
-				outer:
-					for _, d := range a {
-						for j, w := range conjuncts {
-							if i == j {
-								continue
-							}
-							if d.Unify(w).Err() != nil {
-								continue outer
-							}
-						}
-						a[k] = d
-						k++
-					}
-					a = a[:k]
-				}
+				_ = i
+				// TODO: it matters here whether a conjunct is obtained
+				// from embedding or normal unification. Fix this at some
+				// point.
+				//
+				// if len(a) > 1 {
+				// 	// Filter disjuncts that cannot unify with other conjuncts,
+				// 	// and thus can never be satisfied.
+				// 	// TODO: there should be generalized simplification logic
+				// 	// in CUE (outside of the usual implicit simplifications).
+				// 	k := 0
+				// outer:
+				// 	for _, d := range a {
+				// 		for j, w := range conjuncts {
+				// 			if i == j {
+				// 				continue
+				// 			}
+				// 			if d.Unify(w).Err() != nil {
+				// 				continue outer
+				// 			}
+				// 		}
+				// 		a[k] = d
+				// 		k++
+				// 	}
+				// 	a = a[:k]
+				// }
 				switch len(a) {
 				case 0:
 					// Conjunct entirely eliminated.
@@ -431,7 +419,7 @@
 
 func appendSplit(a []cue.Value, splitBy cue.Op, v cue.Value) []cue.Value {
 	op, args := v.Expr()
-	if op == cue.NoOp && len(args) > 0 {
+	if op == cue.NoOp && len(args) == 1 {
 		// TODO: this is to deal with default value removal. This may change
 		// whe we completely separate default values from values.
 		a = append(a, args...)
@@ -718,6 +706,9 @@
 
 	for i, _ := v.Fields(cue.Optional(true), cue.Definitions(true)); i.Next(); {
 		label := i.Label()
+		if b.ctx.isInternal(label) {
+			continue
+		}
 		var core *builder
 		if b.core != nil {
 			core = b.core.properties[label]
@@ -725,7 +716,10 @@
 		schema := b.schema(core, label, i.Value())
 		switch {
 		case i.IsDefinition():
-			ref := strings.Join(append(b.ctx.path, label), ".")
+			ref := b.ctx.makeRef(b.ctx.instExt, append(b.ctx.path, label))
+			if ref == "" {
+				continue
+			}
 			b.ctx.schemas.Set(ref, schema)
 		case !b.isNonCore() || len(schema.Elts) > 0:
 			properties.Set(label, schema)
diff --git a/encoding/openapi/testdata/builtins.json b/encoding/openapi/testdata/builtins.json
index 91db4e7..8d43e35 100644
--- a/encoding/openapi/testdata/builtins.json
+++ b/encoding/openapi/testdata/builtins.json
@@ -5,7 +5,6 @@
    "components": {
       "schemas": {
          "Duration": {
-            "description": "This is not an OpenAPI type and has no format. In this case\nwe map to a type so that it can be documented properly (without\nrepeating it).",
             "type": "string"
          },
          "MyStruct": {
diff --git a/encoding/openapi/testdata/nested.cue b/encoding/openapi/testdata/nested.cue
index f1343e0..a95b2b0 100644
--- a/encoding/openapi/testdata/nested.cue
+++ b/encoding/openapi/testdata/nested.cue
@@ -4,5 +4,8 @@
 	T :: int
 
 	a?: T
-	b?: T
+
+	{b?: T}
+
+	c?: [...T]
 }
diff --git a/encoding/openapi/testdata/nested.json b/encoding/openapi/testdata/nested.json
index d390c64..c392b15 100644
--- a/encoding/openapi/testdata/nested.json
+++ b/encoding/openapi/testdata/nested.json
@@ -10,6 +10,12 @@
                "a": {
                   "$ref": "#/components/schemas/Struct.T"
                },
+               "c": {
+                  "type": "array",
+                  "items": {
+                     "$ref": "#/components/schemas/Struct.T"
+                  }
+               },
                "b": {
                   "$ref": "#/components/schemas/Struct.T"
                }
diff --git a/encoding/openapi/testdata/oneof-funcs.json b/encoding/openapi/testdata/oneof-funcs.json
index 19c7fe3..b61f51b 100644
--- a/encoding/openapi/testdata/oneof-funcs.json
+++ b/encoding/openapi/testdata/oneof-funcs.json
@@ -16,59 +16,123 @@
                   "type": "integer"
                }
             },
-            "oneOf": [
+            "allOf": [
                {
-                  "not": {
-                     "anyOf": [
-                        {
-                           "required": [
-                              "b"
-                           ],
-                           "properties": {
-                              "b": {
-                                 "description": "Randomly picked description from a set of size one.",
-                                 "type": "integer"
+                  "oneOf": [
+                     {
+                        "not": {
+                           "anyOf": [
+                              {
+                                 "required": [
+                                    "b"
+                                 ],
+                                 "properties": {
+                                    "b": {
+                                       "$ref": "#/components/schemas/EMBED_T"
+                                    }
+                                 }
+                              },
+                              {
+                                 "required": [
+                                    "c"
+                                 ],
+                                 "properties": {
+                                    "c": {
+                                       "description": "Randomly picked description from a set of size one.",
+                                       "type": "integer"
+                                    }
+                                 }
                               }
-                           }
-                        },
-                        {
-                           "required": [
-                              "c"
-                           ],
-                           "properties": {
-                              "c": {
-                                 "description": "Randomly picked description from a set of size one.",
-                                 "type": "integer"
-                              }
+                           ]
+                        }
+                     },
+                     {
+                        "required": [
+                           "b"
+                        ],
+                        "properties": {
+                           "b": {
+                              "$ref": "#/components/schemas/EMBED_T"
                            }
                         }
-                     ]
-                  }
+                     },
+                     {
+                        "required": [
+                           "c"
+                        ],
+                        "properties": {
+                           "c": {
+                              "description": "Randomly picked description from a set of size one.",
+                              "type": "integer"
+                           }
+                        }
+                     }
+                  ]
                },
                {
-                  "required": [
-                     "b"
-                  ],
-                  "properties": {
-                     "b": {
-                        "description": "Randomly picked description from a set of size one.",
-                        "type": "integer"
+                  "oneOf": [
+                     {
+                        "not": {
+                           "anyOf": [
+                              {
+                                 "required": [
+                                    "d"
+                                 ],
+                                 "properties": {
+                                    "d": {
+                                       "$ref": "#/components/schemas/EMBED_T"
+                                    }
+                                 }
+                              },
+                              {
+                                 "required": [
+                                    "e"
+                                 ],
+                                 "properties": {
+                                    "e": {
+                                       "description": "Randomly picked description from a set of size one.",
+                                       "type": "integer"
+                                    }
+                                 }
+                              }
+                           ]
+                        }
+                     },
+                     {
+                        "required": [
+                           "d"
+                        ],
+                        "properties": {
+                           "d": {
+                              "$ref": "#/components/schemas/EMBED_T"
+                           }
+                        }
+                     },
+                     {
+                        "required": [
+                           "e"
+                        ],
+                        "properties": {
+                           "e": {
+                              "description": "Randomly picked description from a set of size one.",
+                              "type": "integer"
+                           }
+                        }
                      }
-                  }
-               },
-               {
-                  "required": [
-                     "c"
-                  ],
-                  "properties": {
-                     "c": {
-                        "description": "Randomly picked description from a set of size one.",
-                        "type": "integer"
-                     }
-                  }
+                  ]
                }
             ]
          },
+         "EMBED_T": {
+            "description": "Randomly picked description from a set of size one.",
+            "type": "object",
+            "properties": {
+               "b": {
+                  "description": "Randomly picked description from a set of size one.",
+                  "type": "integer"
+               }
+            }
+         },
          "FOO": {
             "description": "Randomly picked description from a set of size one.",
             "type": "object",
diff --git a/encoding/openapi/testdata/oneof-resolve.json b/encoding/openapi/testdata/oneof-resolve.json
index 038f2ac..2b894b0 100644
--- a/encoding/openapi/testdata/oneof-resolve.json
+++ b/encoding/openapi/testdata/oneof-resolve.json
@@ -13,42 +13,100 @@
                "a": {
                   "type": "integer"
                },
-               "b": {
+               "d": {
+                  "type": "object",
+                  "properties": {
+                     "b": {
+                        "type": "integer"
+                     }
+                  }
+               },
+               "e": {
                   "type": "integer"
                },
+               "b": {
+                  "type": "object",
+                  "properties": {
+                     "b": {
+                        "type": "integer"
+                     }
+                  }
+               },
                "c": {
                   "type": "integer"
                }
             },
-            "oneOf": [
+            "allOf": [
                {
-                  "not": {
-                     "anyOf": [
-                        {
-                           "required": [
-                              "b"
-                           ]
-                        },
-                        {
-                           "required": [
-                              "c"
+                  "oneOf": [
+                     {
+                        "not": {
+                           "anyOf": [
+                              {
+                                 "required": [
+                                    "b"
+                                 ]
+                              },
+                              {
+                                 "required": [
+                                    "c"
+                                 ]
+                              }
                            ]
                         }
-                     ]
-                  }
-               },
-               {
-                  "required": [
-                     "b"
+                     },
+                     {
+                        "required": [
+                           "b"
+                        ]
+                     },
+                     {
+                        "required": [
+                           "c"
+                        ]
+                     }
                   ]
                },
                {
-                  "required": [
-                     "c"
+                  "oneOf": [
+                     {
+                        "not": {
+                           "anyOf": [
+                              {
+                                 "required": [
+                                    "d"
+                                 ]
+                              },
+                              {
+                                 "required": [
+                                    "e"
+                                 ]
+                              }
+                           ]
+                        }
+                     },
+                     {
+                        "required": [
+                           "d"
+                        ]
+                     },
+                     {
+                        "required": [
+                           "e"
+                        ]
+                     }
                   ]
                }
             ]
          },
+         "Embed.T": {
+            "type": "object",
+            "properties": {
+               "b": {
+                  "type": "integer"
+               }
+            }
+         },
          "Foo": {
             "type": "object",
             "required": [
diff --git a/encoding/openapi/testdata/oneof.cue b/encoding/openapi/testdata/oneof.cue
index 6fa37d4..a78285d 100644
--- a/encoding/openapi/testdata/oneof.cue
+++ b/encoding/openapi/testdata/oneof.cue
@@ -47,9 +47,15 @@
 	a?: int
 
 	close({}) |
-	close({b: int}) |
+	close({b: T}) |
 	close({c: int})
 
+	T :: {b?: int}
+
+	close({}) |
+	close({d: T}) |
+	close({e: int})
+
 	// TODO: maybe support builtin to write this as
 	// oneof({},
 	// {b: int},
diff --git a/encoding/openapi/testdata/oneof.json b/encoding/openapi/testdata/oneof.json
index 48ce218..6f0c9f6 100644
--- a/encoding/openapi/testdata/oneof.json
+++ b/encoding/openapi/testdata/oneof.json
@@ -11,55 +11,117 @@
                   "type": "integer"
                }
             },
-            "oneOf": [
+            "allOf": [
                {
-                  "not": {
-                     "anyOf": [
-                        {
-                           "required": [
-                              "b"
-                           ],
-                           "properties": {
-                              "b": {
-                                 "type": "integer"
+                  "oneOf": [
+                     {
+                        "not": {
+                           "anyOf": [
+                              {
+                                 "required": [
+                                    "b"
+                                 ],
+                                 "properties": {
+                                    "b": {
+                                       "$ref": "#/components/schemas/Embed.T"
+                                    }
+                                 }
+                              },
+                              {
+                                 "required": [
+                                    "c"
+                                 ],
+                                 "properties": {
+                                    "c": {
+                                       "type": "integer"
+                                    }
+                                 }
                               }
-                           }
-                        },
-                        {
-                           "required": [
-                              "c"
-                           ],
-                           "properties": {
-                              "c": {
-                                 "type": "integer"
-                              }
+                           ]
+                        }
+                     },
+                     {
+                        "required": [
+                           "b"
+                        ],
+                        "properties": {
+                           "b": {
+                              "$ref": "#/components/schemas/Embed.T"
                            }
                         }
-                     ]
-                  }
+                     },
+                     {
+                        "required": [
+                           "c"
+                        ],
+                        "properties": {
+                           "c": {
+                              "type": "integer"
+                           }
+                        }
+                     }
+                  ]
                },
                {
-                  "required": [
-                     "b"
-                  ],
-                  "properties": {
-                     "b": {
-                        "type": "integer"
+                  "oneOf": [
+                     {
+                        "not": {
+                           "anyOf": [
+                              {
+                                 "required": [
+                                    "d"
+                                 ],
+                                 "properties": {
+                                    "d": {
+                                       "$ref": "#/components/schemas/Embed.T"
+                                    }
+                                 }
+                              },
+                              {
+                                 "required": [
+                                    "e"
+                                 ],
+                                 "properties": {
+                                    "e": {
+                                       "type": "integer"
+                                    }
+                                 }
+                              }
+                           ]
+                        }
+                     },
+                     {
+                        "required": [
+                           "d"
+                        ],
+                        "properties": {
+                           "d": {
+                              "$ref": "#/components/schemas/Embed.T"
+                           }
+                        }
+                     },
+                     {
+                        "required": [
+                           "e"
+                        ],
+                        "properties": {
+                           "e": {
+                              "type": "integer"
+                           }
+                        }
                      }
-                  }
-               },
-               {
-                  "required": [
-                     "c"
-                  ],
-                  "properties": {
-                     "c": {
-                        "type": "integer"
-                     }
-                  }
+                  ]
                }
             ]
          },
+         "Embed.T": {
+            "type": "object",
+            "properties": {
+               "b": {
+                  "type": "integer"
+               }
+            }
+         },
          "Foo": {
             "type": "object",
             "required": [
diff --git a/encoding/openapi/testdata/openapi-norefs.json b/encoding/openapi/testdata/openapi-norefs.json
index d39a589..2839706 100644
--- a/encoding/openapi/testdata/openapi-norefs.json
+++ b/encoding/openapi/testdata/openapi-norefs.json
@@ -177,6 +177,11 @@
             "oneOf": [
                {
                   "required": [
+                     "a"
+                  ]
+               },
+               {
+                  "required": [
                      "b"
                   ]
                },
diff --git a/encoding/openapi/testdata/openapi.json b/encoding/openapi/testdata/openapi.json
index ef7d41c..8f8502f 100644
--- a/encoding/openapi/testdata/openapi.json
+++ b/encoding/openapi/testdata/openapi.json
@@ -166,6 +166,16 @@
             "oneOf": [
                {
                   "required": [
+                     "a"
+                  ],
+                  "properties": {
+                     "a": {
+                        "type": "number"
+                     }
+                  }
+               },
+               {
+                  "required": [
                      "b"
                   ],
                   "properties": {
diff --git a/encoding/openapi/types.go b/encoding/openapi/types.go
index 3a82362..39f5c59 100644
--- a/encoding/openapi/types.go
+++ b/encoding/openapi/types.go
@@ -16,6 +16,7 @@
 
 import (
 	"fmt"
+	"strings"
 
 	"github.com/cockroachdb/apd/v2"
 
@@ -38,7 +39,6 @@
 	"bytes":  "binary",
 
 	"time.Time":                  "dateTime",
-	"time.Time ()":               "dateTime",
 	`time.Format ("2006-01-02")`: "date",
 
 	// TODO: if a format is more strict (e.g. using zeros instead of nines
@@ -54,11 +54,26 @@
 	default:
 		return ""
 	}
-	b, err := format.Node(v.Syntax(cue.Final()))
-	if err != nil {
-		return ""
+	var expr, arg string
+	op, a := v.Expr()
+	if op == cue.CallOp {
+		v = a[0]
+		if len(a) == 2 {
+			arg = fmt.Sprintf(" (%s)", a[1].Eval())
+		}
 	}
-	if s, ok := cueToOpenAPI[string(b)]; ok {
+	if inst, ref := v.Reference(); len(ref) > 0 {
+		expr = inst.ImportPath + "." + strings.Join(ref, ".")
+		expr += arg
+	} else {
+		// TODO: have some function to extract normalized builtin types.
+		b, err := format.Node(v.Syntax(cue.Final()))
+		if err != nil {
+			return ""
+		}
+		expr = string(b)
+	}
+	if s, ok := cueToOpenAPI[expr]; ok {
 		return s
 	}
 	s := fmt.Sprint(v)