encoding/openapi: encode title, version, and contact as fields

This replaces the attributes encoding.

As it now consults regular fields as values, this requires
that schemas be encoded as definitions.

Modified Instance.Doc to also fetch doc comments for
files that do not have a package.

This exposed a bug where default values were not made
concrete.

Issue #56

Change-Id: I31572ac0a09eecca23f6c632fb9c9731a9bdd7d2
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5440
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cmd/cue/cmd/testdata/script/def_openapi.txt b/cmd/cue/cmd/testdata/script/def_openapi.txt
index a75de6e..111a5c7 100644
--- a/cmd/cue/cmd/testdata/script/def_openapi.txt
+++ b/cmd/cue/cmd/testdata/script/def_openapi.txt
@@ -11,6 +11,10 @@
 cmp stdout expect-cue
 
 -- foo.cue --
+// Some clever title.
+
+$version: "v1"
+
 Foo :: {
     a: int
     b: uint & <10
@@ -23,7 +27,10 @@
 -- expect-json-out --
 {
     "openapi": "3.0.0",
-    "info": {},
+    "info": {
+        "title": "Some clever title.",
+        "version": "v1"
+    },
     "paths": {},
     "components": {
         "schemas": {
@@ -60,7 +67,9 @@
 }
 -- expect-yaml-out --
 openapi: 3.0.0
-info: {}
+info:
+    title: Some clever title.
+    version: v1
 paths: {}
 components:
     schemas:
@@ -85,7 +94,10 @@
                     exclusiveMaximum: 10
 -- expect-cue-out --
 openapi: "3.0.0"
-info: {}
+info: {
+	title:   "Some clever title."
+	version: "v1"
+}
 paths: {}
 components: schemas: {
 	Bar: {
@@ -107,8 +119,15 @@
 	}
 }
 -- expect-cue --
+
+// Some clever title.
 package foo
 
+info: {
+	title:   "Some clever title."
+	version: "v1"
+}
+
 Bar :: {
 	foo: Foo
 	...
diff --git a/cue/instance.go b/cue/instance.go
index 5d4127a..d26b496 100644
--- a/cue/instance.go
+++ b/cue/instance.go
@@ -171,11 +171,25 @@
 	}
 	for _, f := range inst.inst.Files {
 		pkg, _, _ := internal.PackageInfo(f)
-		if pkg == nil {
-			continue
+		var cgs []*ast.CommentGroup
+		if pkg != nil {
+			cgs = pkg.Comments()
+		} else if cgs = f.Comments(); len(cgs) > 0 {
+			// Use file comment.
+		} else {
+			// Use first comment declaration before any package or import.
+			for _, d := range f.Decls {
+				switch x := d.(type) {
+				case *ast.Attribute:
+					continue
+				case *ast.CommentGroup:
+					cgs = append(cgs, x)
+				}
+				break
+			}
 		}
 		var cg *ast.CommentGroup
-		for _, c := range pkg.Comments() {
+		for _, c := range cgs {
 			if c.Position == 0 {
 				cg = c
 			}
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index 8ceeb2e..1c8b1b6 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -109,6 +109,9 @@
 		return nil, err
 	}
 	for i.Next() {
+		if !i.IsDefinition() {
+			continue
+		}
 		// message, enum, or constant.
 		label := i.Label()
 		if c.isInternal(label) {
@@ -409,7 +412,7 @@
 			fallthrough
 		default:
 			if !b.isNonCore() {
-				e := v.Syntax().(ast.Expr)
+				e := v.Syntax(cue.Concrete(true)).(ast.Expr)
 				b.setFilter("Schema", "default", e)
 			}
 		}
diff --git a/encoding/openapi/decode.go b/encoding/openapi/decode.go
index 7317785..c0ccdce 100644
--- a/encoding/openapi/decode.go
+++ b/encoding/openapi/decode.go
@@ -15,7 +15,6 @@
 package openapi
 
 import (
-	"fmt"
 	"strings"
 
 	"cuelang.org/go/cue"
@@ -86,15 +85,13 @@
 	// 	add(internal.NewAttr("openapi", "version="+ version))
 	// }
 
-	info := v.Lookup("info")
-	if version, _ := info.Lookup("version").String(); version != "" {
-		add(internal.NewAttr("version", version))
+	if info := v.Lookup("info"); info.Exists() {
+		add(&ast.Field{
+			Label: ast.NewIdent("info"),
+			Value: info.Syntax().(ast.Expr),
+		})
 	}
 
-	add(fieldsAttr(info, "license", "name", "url"))
-	add(fieldsAttr(info, "contact", "name", "url", "email"))
-	// TODO: terms of service.
-
 	if i < len(js.Decls) {
 		ast.SetRelPos(js.Decls[i], token.NewSection)
 		f.Decls = append(f.Decls, js.Decls[i:]...)
@@ -103,22 +100,26 @@
 	return f, nil
 }
 
-func fieldsAttr(v cue.Value, name string, fields ...string) ast.Decl {
+func newField(key, value string) *ast.Field {
+	return &ast.Field{
+		Label: ast.NewIdent(key),
+		Value: ast.NewString(value),
+	}
+}
+
+func fieldsInfo(v cue.Value, name string, fields ...string) ast.Decl {
 	group := v.Lookup(name)
 	if !group.Exists() {
 		return nil
 	}
 
-	buf := &strings.Builder{}
+	a := []interface{}{}
 	for _, f := range fields {
 		if s, _ := group.Lookup(f).String(); s != "" {
-			if buf.Len() > 0 {
-				buf.WriteByte(',')
-			}
-			_, _ = fmt.Fprintf(buf, "%s=%q", f, s)
+			a = append(a, ast.NewIdent(f), ast.NewString(s))
 		}
 	}
-	return internal.NewAttr(name, buf.String())
+	return &ast.Field{Label: ast.NewIdent("$" + name), Value: ast.NewStruct(a...)}
 }
 
 const oapiSchemas = "#/components/schemas/"
diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go
index 4f4a92f..9613331 100644
--- a/encoding/openapi/openapi.go
+++ b/encoding/openapi/openapi.go
@@ -16,6 +16,8 @@
 
 import (
 	"encoding/json"
+	"fmt"
+	"strings"
 
 	"cuelang.org/go/cue"
 	"cuelang.org/go/cue/ast"
@@ -87,7 +89,7 @@
 	if err != nil {
 		return nil, err
 	}
-	top, err := c.compose(all)
+	top, err := c.compose(inst, all)
 	if err != nil {
 		return nil, err
 	}
@@ -103,7 +105,7 @@
 	if err != nil {
 		return nil, err
 	}
-	top, err := g.compose(all)
+	top, err := g.compose(inst, all)
 	return (*OrderedMap)(top), err
 }
 
@@ -120,23 +122,84 @@
 
 }
 
-func (c *Config) compose(schemas *ast.StructLit) (x *ast.StructLit, err error) {
+func (c *Config) compose(inst *cue.Instance, schemas *ast.StructLit) (x *ast.StructLit, err error) {
+
+	var errs errors.Error
+
+	var title, version string
+	var info *ast.StructLit
+
+	for i, _ := inst.Value().Fields(cue.Definitions(true)); i.Next(); {
+		if !i.IsDefinition() {
+			label := i.Label()
+			attr := i.Value().Attribute("openapi")
+			if s, _ := attr.String(0); s != "" {
+				label = s
+			}
+			switch label {
+			case "-":
+			case "info":
+				info, _ = i.Value().Syntax().(*ast.StructLit)
+				if info == nil {
+					errs = errors.Append(errs, errors.Newf(i.Value().Pos(),
+						"info must be a struct"))
+				}
+				title, _ = i.Value().Lookup("title").String()
+				version, _ = i.Value().Lookup("version").String()
+
+			default:
+				errs = errors.Append(errs, errors.Newf(i.Value().Pos(),
+					"openapi: unsupported top-level field %q", x))
+			}
+		}
+	}
+
 	// Support of OrderedMap is mostly for backwards compatibility.
-	var info ast.Expr
 	switch x := c.Info.(type) {
 	case nil:
-		info = ast.NewStruct()
-	case ast.Expr:
+		if title == "" {
+			title = "Generated by cue."
+			for _, d := range inst.Doc() {
+				title = strings.TrimSpace(d.Text())
+				break
+			}
+			if p := inst.ImportPath; title == "" && p != "" {
+				title = fmt.Sprintf("Generated by cue from package %q", p)
+			}
+		}
+
+		if version == "" {
+			version, _ = inst.Lookup("$version").String()
+			if version == "" {
+				version = "no version"
+			}
+		}
+
+		if info == nil {
+			info = ast.NewStruct(
+				"title", ast.NewString(title),
+				"version", ast.NewString(version),
+			)
+		} else {
+			m := (*OrderedMap)(info)
+			m.Set("title", ast.NewString(title))
+			m.Set("version", ast.NewString(version))
+		}
+
+	case *ast.StructLit:
 		info = x
 	case *OrderedMap:
 		info = (*ast.StructLit)(x)
 	case OrderedMap:
 		info = (*ast.StructLit)(&x)
 	default:
-		info, err = toCUE("info section", x)
+		x, err := toCUE("info section", x)
 		if err != nil {
 			return nil, err
 		}
+		info, _ = x.(*ast.StructLit)
+		errs = errors.Append(errs, errors.Newf(token.NoPos,
+			"Info field supplied must be an *ast.StructLit"))
 	}
 
 	return ast.NewStruct(
diff --git a/encoding/openapi/testdata/array.cue b/encoding/openapi/testdata/array.cue
index 9f28a52..10cade0 100644
--- a/encoding/openapi/testdata/array.cue
+++ b/encoding/openapi/testdata/array.cue
@@ -1,6 +1,6 @@
 import "list"
 
-Arrays: {
+Arrays :: {
 	bar?: [...MyEnum]
 	foo?: [...MyStruct]
 
@@ -9,17 +9,17 @@
 	qux?: list.MinItems(1) & list.MaxItems(3)
 }
 
-Arrays: {
+Arrays :: {
 	bar?: [...MyEnum]
 	foo?: [...MyStruct]
 }
 
 // MyStruct
-MyStruct: {
+MyStruct :: {
 	a?: int
 	e?: [...MyEnum]
 	e?: [...MyEnum]
 }
 
 // MyEnum
-MyEnum: *"1" | "2" | "3"
+MyEnum :: *"1" | "2" | "3"
diff --git a/encoding/openapi/testdata/array.json b/encoding/openapi/testdata/array.json
index 0f7f9fe..694fb75 100644
--- a/encoding/openapi/testdata/array.json
+++ b/encoding/openapi/testdata/array.json
@@ -1,6 +1,9 @@
 {
    "openapi": "3.0.0",
-   "info": {},
+   "info": {
+      "title": "Generated by cue.",
+      "version": "no version"
+   },
    "paths": {},
    "components": {
       "schemas": {
diff --git a/encoding/openapi/testdata/builtins.cue b/encoding/openapi/testdata/builtins.cue
index 0d094e4..8a7f0a1 100644
--- a/encoding/openapi/testdata/builtins.cue
+++ b/encoding/openapi/testdata/builtins.cue
@@ -5,7 +5,7 @@
 
 _time = time
 
-MyStruct: {
+MyStruct :: {
 	timestamp1?: time.Time
 	timestamp2?: time.Time()
 	timestamp3?: time.Format(time.RFC3339Nano)
diff --git a/encoding/openapi/testdata/builtins.json b/encoding/openapi/testdata/builtins.json
index 8d43e35..aebb3e3 100644
--- a/encoding/openapi/testdata/builtins.json
+++ b/encoding/openapi/testdata/builtins.json
@@ -1,6 +1,9 @@
 {
    "openapi": "3.0.0",
-   "info": {},
+   "info": {
+      "title": "Generated by cue.",
+      "version": "no version"
+   },
    "paths": {},
    "components": {
       "schemas": {
diff --git a/encoding/openapi/testdata/nested.cue b/encoding/openapi/testdata/nested.cue
index a95b2b0..cbdfb36 100644
--- a/encoding/openapi/testdata/nested.cue
+++ b/encoding/openapi/testdata/nested.cue
@@ -1,4 +1,4 @@
-package foo
+// File comment.
 
 Struct :: {
 	T :: int
diff --git a/encoding/openapi/testdata/nested.json b/encoding/openapi/testdata/nested.json
index c392b15..c9bcc7b 100644
--- a/encoding/openapi/testdata/nested.json
+++ b/encoding/openapi/testdata/nested.json
@@ -1,6 +1,9 @@
 {
    "openapi": "3.0.0",
-   "info": {},
+   "info": {
+      "title": "File comment.",
+      "version": "no version"
+   },
    "paths": {},
    "components": {
       "schemas": {
diff --git a/encoding/openapi/testdata/nums.cue b/encoding/openapi/testdata/nums.cue
index d5b19c6..02b7c15 100644
--- a/encoding/openapi/testdata/nums.cue
+++ b/encoding/openapi/testdata/nums.cue
@@ -1,5 +1,5 @@
 import "math"
 
-mul: math.MultipleOf(5)
+mul :: math.MultipleOf(5)
 
-neq: !=4
+neq :: !=4
diff --git a/encoding/openapi/testdata/nums.json b/encoding/openapi/testdata/nums.json
index e26da85..c032f20 100644
--- a/encoding/openapi/testdata/nums.json
+++ b/encoding/openapi/testdata/nums.json
@@ -1,6 +1,9 @@
 {
    "openapi": "3.0.0",
-   "info": {},
+   "info": {
+      "title": "Generated by cue.",
+      "version": "no version"
+   },
    "paths": {},
    "components": {
       "schemas": {
diff --git a/encoding/openapi/testdata/oneof.cue b/encoding/openapi/testdata/oneof.cue
index a78285d..95935ac 100644
--- a/encoding/openapi/testdata/oneof.cue
+++ b/encoding/openapi/testdata/oneof.cue
@@ -1,3 +1,7 @@
+// OpenAPI title.
+
+$version: "v1alpha1"
+
 T :: {
 	shared: int
 }
@@ -17,7 +21,7 @@
 
 MyInt :: int
 
-Foo: {
+Foo :: {
 	include: T
 	exclude: [...T]
 	count: MyInt
diff --git a/encoding/openapi/testdata/oneof.json b/encoding/openapi/testdata/oneof.json
index 6f0c9f6..9238266 100644
--- a/encoding/openapi/testdata/oneof.json
+++ b/encoding/openapi/testdata/oneof.json
@@ -1,6 +1,9 @@
 {
    "openapi": "3.0.0",
-   "info": {},
+   "info": {
+      "title": "OpenAPI title.",
+      "version": "v1alpha1"
+   },
    "paths": {},
    "components": {
       "schemas": {
diff --git a/encoding/openapi/testdata/openapi.cue b/encoding/openapi/testdata/openapi.cue
index c4308e8..2ed3fab 100644
--- a/encoding/openapi/testdata/openapi.cue
+++ b/encoding/openapi/testdata/openapi.cue
@@ -1,7 +1,16 @@
+
+// An OpenAPI testing package.
 package openapi
 
+$version: "v1beta2"
+
+info: {
+	contact: url:  "https://cuelang.org"
+	contact: name: "John Doe"
+}
+
 // MyMessage is my message.
-MyMessage: {
+MyMessage :: {
 	port?: Port & {} @protobuf(1)
 
 	foo: Int32 & >10 & <1000 & int32 @protobuf(2)
@@ -9,30 +18,30 @@
 	bar: [...string] @protobuf(3)
 }
 
-MyMessage: {
+MyMessage :: {
 	// Field a.
 	a: 1
 } | {
 	b: string //2: crash
 }
 
-YourMessage: ({a: number} | {b: string} | {b: number}) & {a?: string}
+YourMessage :: ({a: number} | {b: string} | {b: number}) & {a?: string}
 
-YourMessage2: ({a: number} | {b: number}) &
+YourMessage2 :: ({a: number} | {b: number}) &
 	({c: number} | {d: number}) &
 	({e: number} | {f: number})
 
-Msg2: {b: number} | {a: string}
+Msg2 :: {b: number} | {a: string}
 
-Int32: int32
+Int32 :: int32
 
-Enum: "foo" | "bar" | "baz"
+Enum :: "foo" | "bar" | "baz"
 
-List: [...number] | *[1, 2, 3]
+List :: [...number] | *[1, 2, 3]
 
-DefaultStruct: Port | *{port: 1}
+DefaultStruct :: Port | *{port: 1}
 
-Port: {
+Port :: {
 	port: int
 
 	obj: [...int]
diff --git a/encoding/openapi/testdata/openapi.json b/encoding/openapi/testdata/openapi.json
index 8f8502f..b34244d 100644
--- a/encoding/openapi/testdata/openapi.json
+++ b/encoding/openapi/testdata/openapi.json
@@ -1,6 +1,13 @@
 {
    "openapi": "3.0.0",
-   "info": {},
+   "info": {
+      "contact": {
+         "name": "John Doe",
+         "url": "https://cuelang.org"
+      },
+      "title": "An OpenAPI testing package.",
+      "version": "v1beta2"
+   },
    "paths": {},
    "components": {
       "schemas": {
diff --git a/encoding/openapi/testdata/refs.cue b/encoding/openapi/testdata/refs.cue
index 2b11279..d69e174 100644
--- a/encoding/openapi/testdata/refs.cue
+++ b/encoding/openapi/testdata/refs.cue
@@ -1,13 +1,13 @@
-Keep: {
+Keep :: {
 	// This comment is included
 	excludedStruct: ExcludedStruct
 	excludedInt:    ExcludedInt
 }
 
 // ExcludedStruct is not included in the output.
-ExcludedStruct: {
+ExcludedStruct :: {
 	A: int
 }
 
 // ExcludedInt is not included in the output.
-ExcludedInt: int
+ExcludedInt :: int
diff --git a/encoding/openapi/testdata/script/basics.txtar b/encoding/openapi/testdata/script/basics.txtar
index b7ece27..1d5c153 100644
--- a/encoding/openapi/testdata/script/basics.txtar
+++ b/encoding/openapi/testdata/script/basics.txtar
@@ -27,9 +27,14 @@
 // Users schema
 package foo
 
-@version(v1beta1)
-@contact(name="The CUE Authors",url="https://cuelang.org")
-
+info: {
+	title:   "Users schema"
+	version: "v1beta1"
+	contact: {
+		name: "The CUE Authors"
+		url:  "https://cuelang.org"
+	}
+}
 // A User uses something.
 User :: {
 	name?:    string
diff --git a/encoding/openapi/testdata/simple.cue b/encoding/openapi/testdata/simple.cue
index 003be57..689490c 100644
--- a/encoding/openapi/testdata/simple.cue
+++ b/encoding/openapi/testdata/simple.cue
@@ -1,4 +1,4 @@
-MyStruct: {
+MyStruct :: {
 	mediumNum: int32
 	smallNum:  int8
 
diff --git a/encoding/openapi/testdata/strings.cue b/encoding/openapi/testdata/strings.cue
index f0d61fe..74ea59e 100644
--- a/encoding/openapi/testdata/strings.cue
+++ b/encoding/openapi/testdata/strings.cue
@@ -1,6 +1,6 @@
 import "strings"
 
-MyType: {
+MyType :: {
 	myString: strings.MinRunes(1) & strings.MaxRunes(5)
 
 	myPattern: =~"foo.*bar"
diff --git a/encoding/openapi/testdata/strings.json b/encoding/openapi/testdata/strings.json
index 8a2c574..2a5411f 100644
--- a/encoding/openapi/testdata/strings.json
+++ b/encoding/openapi/testdata/strings.json
@@ -1,6 +1,9 @@
 {
    "openapi": "3.0.0",
-   "info": {},
+   "info": {
+      "title": "Generated by cue.",
+      "version": "no version"
+   },
    "paths": {},
    "components": {
       "schemas": {
diff --git a/encoding/openapi/testdata/struct.cue b/encoding/openapi/testdata/struct.cue
index 875c714..0000954 100644
--- a/encoding/openapi/testdata/struct.cue
+++ b/encoding/openapi/testdata/struct.cue
@@ -1,8 +1,8 @@
 import "struct"
 
-MyMap: struct.MinFields(4)
-MyMap: struct.MaxFields(9)
+MyMap :: struct.MinFields(4)
+MyMap :: struct.MaxFields(9)
 
-MyType: {
+MyType :: {
 	map: MyMap
 }
diff --git a/encoding/openapi/testdata/struct.json b/encoding/openapi/testdata/struct.json
index d2b5c6b..3af6ee3 100644
--- a/encoding/openapi/testdata/struct.json
+++ b/encoding/openapi/testdata/struct.json
@@ -1,6 +1,9 @@
 {
    "openapi": "3.0.0",
-   "info": {},
+   "info": {
+      "title": "Generated by cue.",
+      "version": "no version"
+   },
    "paths": {},
    "components": {
       "schemas": {
diff --git a/encoding/openapi/testdata/structural.cue b/encoding/openapi/testdata/structural.cue
index 099fad7..9202681 100644
--- a/encoding/openapi/testdata/structural.cue
+++ b/encoding/openapi/testdata/structural.cue
@@ -1,6 +1,6 @@
 import "time"
 
-Attributes: {
+Attributes :: {
 	//  A map of attribute name to its value.
 	attributes: {
 		[string]: AttrValue
@@ -8,9 +8,9 @@
 }
 
 //  The attribute value.
-AttrValue: {}
+AttrValue :: {}
 
-AttrValue: {
+AttrValue :: {
 	//  Used for values of type STRING, DNS_NAME, EMAIL_ADDRESS, and URI
 	stringValue: string @protobuf(2,name=string_value)
 } | {
@@ -36,7 +36,7 @@
 	stringMapValue: Attributes_StringMap @protobuf(9,type=StringMap,name=string_map_value)
 }
 
-Attributes_StringMap: {
+Attributes_StringMap :: {
 	//  Holds a set of name/value pairs.
 	entries: {
 		[string]: string