encoding/openapi: support time types

Also recognize unsupported builtins for
their type.

Issue #56

Change-Id: Ic900829144d0130db412c1202f82df4e3a56c236
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2724
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index fa74d5b..bcb21d4 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -209,7 +209,7 @@
 	count := 0
 	disallowDefault := false
 	var values cue.Value
-	if b.ctx.expandRefs {
+	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.
@@ -760,7 +760,6 @@
 		}
 
 	case cue.NoOp, cue.SelectorOp:
-		// TODO: determine formats from specific types.
 
 	case cue.CallOp:
 		name := fmt.Sprint(a[0])
diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go
index 9fbe314..bcdaf19 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -63,6 +63,10 @@
 		"nums.json",
 		defaultConfig,
 	}, {
+		"builtins.cue",
+		"builtins.json",
+		defaultConfig,
+	}, {
 		"oneof.cue",
 		"oneof.json",
 		defaultConfig,
diff --git a/encoding/openapi/testdata/builtins.cue b/encoding/openapi/testdata/builtins.cue
new file mode 100644
index 0000000..0d094e4
--- /dev/null
+++ b/encoding/openapi/testdata/builtins.cue
@@ -0,0 +1,22 @@
+import (
+	"time"
+	"list"
+)
+
+_time = time
+
+MyStruct: {
+	timestamp1?: time.Time
+	timestamp2?: time.Time()
+	timestamp3?: time.Format(time.RFC3339Nano)
+	timestamp4?: _time.Time
+	date1?:      time.Format(time.RFC3339Date)
+	date2?:      _time.Format(time.RFC3339Date)
+
+	// This is not an OpenAPI type and has no format. In this case
+	// we map to a type so that it can be documented properly (without
+	// repeating it).
+	timeout?: time.Duration
+
+	contains: list.Contains("foo") // not supported, but should be recognized as list
+}
diff --git a/encoding/openapi/testdata/builtins.json b/encoding/openapi/testdata/builtins.json
new file mode 100644
index 0000000..1442f6c
--- /dev/null
+++ b/encoding/openapi/testdata/builtins.json
@@ -0,0 +1,50 @@
+{
+   "openapi": "3.0.0",
+   "info": {},
+   "components": {
+      "schemas": {
+         "MyStruct": {
+            "type": "object",
+            "required": [
+               "contains"
+            ],
+            "properties": {
+               "timestamp1": {
+                  "type": "string",
+                  "format": "dateTime"
+               },
+               "timestamp2": {
+                  "type": "string",
+                  "format": "dateTime"
+               },
+               "timestamp3": {
+                  "type": "string",
+                  "format": "dateTime"
+               },
+               "timestamp4": {
+                  "type": "string",
+                  "format": "dateTime"
+               },
+               "date1": {
+                  "type": "string",
+                  "format": "date"
+               },
+               "date2": {
+                  "type": "string",
+                  "format": "date"
+               },
+               "timeout": {
+                  "$ref": "#/components/schemas/Duration"
+               },
+               "contains": {
+                  "type": "array"
+               }
+            }
+         },
+         "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"
+         }
+      }
+   }
+}
\ No newline at end of file
diff --git a/encoding/openapi/types.go b/encoding/openapi/types.go
index 3995055..e30a4ba 100644
--- a/encoding/openapi/types.go
+++ b/encoding/openapi/types.go
@@ -15,6 +15,8 @@
 package openapi
 
 import (
+	"fmt"
+
 	"github.com/cockroachdb/apd/v2"
 
 	"cuelang.org/go/cue"
@@ -32,7 +34,15 @@
 	"string": "string",
 	"bytes":  "binary",
 
-	// TODO: date, date-time, password.
+	"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
+	// for fractional seconds), we could still use this as an approximation.
+	`time.Format ("2006-01-02T15:04:05.999999999Z07:00")`: "dateTime",
+
+	// TODO:  password.
 }
 
 func extractFormat(v cue.Value) string {
@@ -45,7 +55,11 @@
 	if err != nil {
 		return ""
 	}
-	return cueToOpenAPI[string(b)]
+	if s, ok := cueToOpenAPI[string(b)]; ok {
+		return s
+	}
+	s := fmt.Sprint(v)
+	return cueToOpenAPI[s]
 }
 
 func simplify(b *builder, t *OrderedMap) {