encoding/openapi: generate CUE expressions

instead of OrderedMap (now deprecated)

This allows the package to be integrated with
the new encoding infrastrucure.

The CUE expressions can, in turn, be used to generate
CUE, JSON, or YAML.

Change-Id: I720ce71779cda6d91ad32098e289d30e1e9a2f1f
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/5184
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index 405b21c..f75181f 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -17,16 +17,14 @@
 import (
 	"fmt"
 	"math"
-	"math/big"
 	"path"
 	"regexp"
 	"sort"
 	"strconv"
 	"strings"
 
-	"github.com/cockroachdb/apd/v2"
-
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/token"
 )
@@ -60,7 +58,7 @@
 
 type typeFunc func(b *builder, a cue.Value)
 
-func schemas(g *Generator, inst *cue.Instance) (schemas *OrderedMap, err error) {
+func schemas(g *Generator, inst *cue.Instance) (schemas *ast.StructLit, err error) {
 	var fieldFilter *regexp.Regexp
 	if g.FieldFilter != "" {
 		fieldFilter, err = regexp.Compile(g.FieldFilter)
@@ -136,10 +134,10 @@
 		}
 	}
 
-	return c.schemas, nil
+	return (*ast.StructLit)(c.schemas), nil
 }
 
-func (c *buildContext) build(name string, v cue.Value) *oaSchema {
+func (c *buildContext) build(name string, v cue.Value) *ast.StructLit {
 	return newCoreBuilder(c).schema(nil, name, v)
 }
 
@@ -171,7 +169,7 @@
 	}
 }
 
-func (b *builder) schema(core *builder, name string, v cue.Value) *oaSchema {
+func (b *builder) schema(core *builder, name string, v cue.Value) *ast.StructLit {
 	oldPath := b.ctx.path
 	b.ctx.path = append(b.ctx.path, name)
 	defer func() { b.ctx.path = oldPath }()
@@ -202,11 +200,11 @@
 	}
 	if len(doc) > 0 {
 		str := strings.TrimSpace(strings.Join(doc, "\n\n"))
-		b.setSingle("description", str, true)
+		b.setSingle("description", ast.NewString(str), true)
 	}
 }
 
-func (b *builder) fillSchema(v cue.Value) *oaSchema {
+func (b *builder) fillSchema(v cue.Value) *ast.StructLit {
 	if b.filled != nil {
 		return b.filled
 	}
@@ -227,23 +225,36 @@
 	}
 
 	schema := b.finish()
+	s := (*ast.StructLit)(schema)
 
-	simplify(b, schema)
+	simplify(b, s)
 
-	sortSchema(schema)
+	sortSchema(s)
 
-	b.filled = schema
-	return schema
+	b.filled = s
+	return s
 }
 
-func sortSchema(s *oaSchema) {
-	sort.Slice(s.kvs, func(i, j int) bool {
-		pi := fieldOrder[s.kvs[i].Key]
-		pj := fieldOrder[s.kvs[j].Key]
+func label(d ast.Decl) string {
+	f := d.(*ast.Field)
+	s, _, _ := ast.LabelName(f.Label)
+	return s
+}
+
+func value(d ast.Decl) ast.Expr {
+	return d.(*ast.Field).Value
+}
+
+func sortSchema(s *ast.StructLit) {
+	sort.Slice(s.Elts, func(i, j int) bool {
+		iName := label(s.Elts[i])
+		jName := label(s.Elts[j])
+		pi := fieldOrder[iName]
+		pj := fieldOrder[jName]
 		if pi != pj {
 			return pi > pj
 		}
-		return s.kvs[i].Key < s.kvs[j].Key
+		return iName < jName
 	})
 }
 
@@ -343,7 +354,7 @@
 			case isConcrete(v):
 				b.dispatch(f, v)
 				if !b.isNonCore() {
-					b.set("enum", []interface{}{b.decode(v)})
+					b.set("enum", ast.NewList(b.decode(v)))
 				}
 			default:
 				if a := appendSplit(nil, cue.OrOp, v); len(a) > 1 {
@@ -373,7 +384,8 @@
 			fallthrough
 		default:
 			if !b.isNonCore() {
-				b.setFilter("Schema", "default", v)
+				e := v.Syntax().(ast.Expr)
+				b.setFilter("Schema", "default", e)
 			}
 		}
 	}
@@ -439,8 +451,8 @@
 
 func (b *builder) disjunction(a []cue.Value, f typeFunc) {
 	disjuncts := []cue.Value{}
-	enums := []interface{}{} // TODO: unique the enums
-	nullable := false        // Only supported in OpenAPI, not JSON schema
+	enums := []ast.Expr{} // TODO: unique the enums
+	nullable := false     // Only supported in OpenAPI, not JSON schema
 
 	for _, v := range a {
 		switch {
@@ -462,17 +474,17 @@
 			b.value(disjuncts[0], f)
 		}
 		if len(enums) > 0 && !b.isNonCore() {
-			b.set("enum", enums)
+			b.set("enum", ast.NewList(enums...))
 		}
 		if nullable {
-			b.setSingle("nullable", true, true) // allowed in Structural
+			b.setSingle("nullable", ast.NewBool(true), true) // allowed in Structural
 		}
 		return
 	}
 
-	anyOf := []*oaSchema{}
+	anyOf := []ast.Expr{}
 	if len(enums) > 0 {
-		anyOf = append(anyOf, b.kv("enum", enums))
+		anyOf = append(anyOf, b.kv("enum", ast.NewList(enums...)))
 	}
 
 	hasEmpty := false
@@ -480,21 +492,21 @@
 		c := newOASBuilder(b)
 		c.value(v, f)
 		t := c.finish()
-		if len(t.kvs) == 0 {
+		if len(t.Elts) == 0 {
 			hasEmpty = true
 		}
-		anyOf = append(anyOf, t)
+		anyOf = append(anyOf, (*ast.StructLit)(t))
 	}
 
 	// If any of the types was "any", a oneOf may be discarded.
 	if !hasEmpty {
-		b.set("oneOf", anyOf)
+		b.set("oneOf", ast.NewList(anyOf...))
 	}
 
 	// TODO: analyze CUE structs to figure out if it should be oneOf or
 	// anyOf. As the source is protobuf for now, it is always oneOf.
 	if nullable {
-		b.setSingle("nullable", true, true)
+		b.setSingle("nullable", ast.NewBool(true), true)
 	}
 }
 
@@ -531,7 +543,7 @@
 	case cue.NullKind:
 		// TODO: for JSON schema we would set the type here. For OpenAPI,
 		// it must be nullable.
-		b.setSingle("nullable", true, true)
+		b.setSingle("nullable", ast.NewBool(true), true)
 
 	case cue.BoolKind:
 		b.setType("boolean", "")
@@ -619,12 +631,12 @@
 		return
 	}
 
-	required := []string{}
+	required := []ast.Expr{}
 	for i, _ := v.Fields(); i.Next(); {
-		required = append(required, i.Label())
+		required = append(required, ast.NewString(i.Label()))
 	}
 	if len(required) > 0 {
-		b.setFilter("Schema", "required", required)
+		b.setFilter("Schema", "required", ast.NewList(required...))
 	}
 
 	var properties *OrderedMap
@@ -643,18 +655,18 @@
 			core = b.core.properties[label]
 		}
 		schema := b.schema(core, label, i.Value())
-		if !b.isNonCore() || len(schema.kvs) > 0 {
+		if !b.isNonCore() || len(schema.Elts) > 0 {
 			properties.Set(label, schema)
 		}
 	}
 
-	if !hasProps && len(properties.kvs) > 0 {
-		b.setSingle("properties", properties, false)
+	if !hasProps && properties.len() > 0 {
+		b.setSingle("properties", (*ast.StructLit)(properties), false)
 	}
 
 	if t, ok := v.Elem(); ok && (b.core == nil || b.core.items == nil) {
 		schema := b.schema(nil, "*", t)
-		if len(schema.kvs) > 0 {
+		if len(schema.Elts) > 0 {
 			b.setSingle("additionalProperties", schema, true) // Not allowed in structural.
 		}
 	}
@@ -693,7 +705,7 @@
 		switch name {
 		case "list.UniqueItems":
 			b.checkArgs(a, 0)
-			b.setFilter("Schema", "uniqueItems", true)
+			b.setFilter("Schema", "uniqueItems", ast.NewBool(true))
 			return
 
 		case "list.MinItems":
@@ -725,7 +737,7 @@
 	//   - unique items: at most one, but idempotent if multiple.
 	// There is never a need for allOf or anyOf. Note that a CUE list
 	// corresponds almost one-to-one to OpenAPI lists.
-	items := []*oaSchema{}
+	items := []ast.Expr{}
 	count := 0
 	for i, _ := v.List(); i.Next(); count++ {
 		items = append(items, b.schema(nil, strconv.Itoa(count), i.Value()))
@@ -734,7 +746,7 @@
 		// TODO: per-item schema are not allowed in OpenAPI, only in JSON Schema.
 		// Perhaps we should turn this into an OR after first normalizing
 		// the entries.
-		b.set("items", items)
+		b.set("items", ast.NewList(items...))
 		// panic("per-item types not supported in OpenAPI")
 	}
 
@@ -761,7 +773,7 @@
 			t := b.schema(core, "*", typ)
 			if len(items) > 0 {
 				b.setFilter("Schema", "additionalItems", t) // Not allowed in structural.
-			} else if !b.isNonCore() || len(t.kvs) > 0 {
+			} else if !b.isNonCore() || len(t.Elts) > 0 {
 				b.setSingle("items", t, true)
 			}
 		}
@@ -771,23 +783,23 @@
 func (b *builder) listCap(v cue.Value) {
 	switch op, a := v.Expr(); op {
 	case cue.LessThanOp:
-		b.setFilter("Schema", "maxItems", b.int(a[0])-1)
+		b.setFilter("Schema", "maxItems", b.inta(a[0], -1))
 	case cue.LessThanEqualOp:
-		b.setFilter("Schema", "maxItems", b.int(a[0]))
+		b.setFilter("Schema", "maxItems", b.inta(a[0], 0))
 	case cue.GreaterThanOp:
-		b.setFilter("Schema", "minItems", b.int(a[0])+1)
+		b.setFilter("Schema", "minItems", b.inta(a[0], 1))
 	case cue.GreaterThanEqualOp:
-		if b.int(a[0]) > 0 {
-			b.setFilter("Schema", "minItems", b.int(a[0]))
+		if b.int64(a[0]) > 0 {
+			b.setFilter("Schema", "minItems", b.inta(a[0], 0))
 		}
 	case cue.NoOp:
 		// must be type, so okay.
 	case cue.NotEqualOp:
 		i := b.int(a[0])
-		b.setNot("allOff", []*oaSchema{
+		b.setNot("allOff", ast.NewList(
 			b.kv("minItems", i),
 			b.kv("maxItems", i),
-		})
+		))
 
 	default:
 		b.failf(v, "unsupported op for list capacity %v", op)
@@ -814,10 +826,10 @@
 
 	case cue.NotEqualOp:
 		i := b.big(a[0])
-		b.setNot("allOff", []*oaSchema{
+		b.setNot("allOff", ast.NewList(
 			b.kv("minimum", i),
 			b.kv("maximum", i),
-		})
+		))
 
 	case cue.CallOp:
 		name := fmt.Sprint(a[0])
@@ -891,9 +903,9 @@
 			return
 		}
 		if op == cue.RegexMatchOp {
-			b.setFilter("Schema", "pattern", s)
+			b.setFilter("Schema", "pattern", ast.NewString(s))
 		} else {
-			b.setNot("pattern", s)
+			b.setNot("pattern", ast.NewString(s))
 		}
 
 	case cue.NoOp, cue.SelectorOp:
@@ -934,10 +946,11 @@
 			return
 		}
 
+		e := ast.NewString(string(s))
 		if op == cue.RegexMatchOp {
-			b.setFilter("Schema", "pattern", s)
+			b.setFilter("Schema", "pattern", e)
 		} else {
-			b.setNot("pattern", s)
+			b.setNot("pattern", e)
 		}
 
 		// TODO: support the following JSON schema constraints
@@ -957,13 +970,13 @@
 	format       string
 	singleFields *oaSchema
 	current      *oaSchema
-	allOf        []*oaSchema
+	allOf        []*ast.StructLit
 	deprecated   bool
 
 	// Building structural schema
 	core       *builder
 	kind       cue.Kind
-	filled     *oaSchema
+	filled     *ast.StructLit
 	values     []cue.Value // in structural mode, all values of not and *Of.
 	keys       []string
 	properties map[string]*builder
@@ -1005,19 +1018,19 @@
 	if b.typ != "" {
 		if b.core == nil || (b.core.typ != b.typ && !b.ctx.structural) {
 			if !t.exists("type") {
-				t.Set("type", b.typ)
+				t.Set("type", ast.NewString(b.typ))
 			}
 		}
 	}
 	if b.format != "" {
 		if b.core == nil || b.core.format != b.format {
-			t.Set("format", b.format)
+			t.Set("format", ast.NewString(b.format))
 		}
 	}
 }
 
 // setFilter is like set, but allows the key-value pair to be filtered.
-func (b *builder) setFilter(schema, key string, v interface{}) {
+func (b *builder) setFilter(schema, key string, v ast.Expr) {
 	if re := b.ctx.fieldFilter; re != nil && re.MatchString(path.Join(schema, key)) {
 		return
 	}
@@ -1025,7 +1038,7 @@
 }
 
 // setSingle sets a value of which there should only be one.
-func (b *builder) setSingle(key string, v interface{}, drop bool) {
+func (b *builder) setSingle(key string, v ast.Expr, drop bool) {
 	if b.singleFields == nil {
 		b.singleFields = &OrderedMap{}
 	}
@@ -1037,30 +1050,28 @@
 	b.singleFields.Set(key, v)
 }
 
-func (b *builder) set(key string, v interface{}) {
+func (b *builder) set(key string, v ast.Expr) {
 	if b.current == nil {
 		b.current = &OrderedMap{}
-		b.allOf = append(b.allOf, b.current)
+		b.allOf = append(b.allOf, (*ast.StructLit)(b.current))
 	} else if b.current.exists(key) {
 		b.current = &OrderedMap{}
-		b.allOf = append(b.allOf, b.current)
+		b.allOf = append(b.allOf, (*ast.StructLit)(b.current))
 	}
 	b.current.Set(key, v)
 }
 
-func (b *builder) kv(key string, value interface{}) *oaSchema {
-	constraint := &OrderedMap{}
-	constraint.Set(key, value)
-	return constraint
+func (b *builder) kv(key string, value ast.Expr) *ast.StructLit {
+	return ast.NewStruct(key, value)
 }
 
-func (b *builder) setNot(key string, value interface{}) {
-	not := &OrderedMap{}
-	not.Set("not", b.kv(key, value))
-	b.add(not)
+func (b *builder) setNot(key string, value ast.Expr) {
+	b.add(ast.NewStruct("not", b.kv(key, value)))
 }
 
-func (b *builder) finish() (t *oaSchema) {
+func (b *builder) finish() *ast.StructLit {
+	var t *OrderedMap
+
 	if b.filled != nil {
 		return b.filled
 	}
@@ -1069,38 +1080,42 @@
 		t = &OrderedMap{}
 
 	case 1:
-		t = b.allOf[0]
+		t = (*OrderedMap)(b.allOf[0])
 
 	default:
+		exprs := []ast.Expr{}
+		for _, s := range b.allOf {
+			exprs = append(exprs, s)
+		}
 		t = &OrderedMap{}
-		t.Set("allOf", b.allOf)
+		t.Set("allOf", ast.NewList(exprs...))
 	}
 	if b.singleFields != nil {
-		b.singleFields.kvs = append(b.singleFields.kvs, t.kvs...)
+		b.singleFields.Elts = append(b.singleFields.Elts, t.Elts...)
 		t = b.singleFields
 	}
 	if b.deprecated {
-		t.Set("deprecated", true)
+		t.Set("deprecated", ast.NewBool(true))
 	}
 	setType(t, b)
-	sortSchema(t)
-	return t
+	sortSchema((*ast.StructLit)(t))
+	return (*ast.StructLit)(t)
 }
 
-func (b *builder) add(t *oaSchema) {
+func (b *builder) add(t *ast.StructLit) {
 	b.allOf = append(b.allOf, t)
 }
 
 func (b *builder) addConjunct(f func(*builder)) {
 	c := newOASBuilder(b)
 	f(c)
-	b.add(c.finish())
+	b.add((*ast.StructLit)(c.finish()))
 }
 
 func (b *builder) addRef(v cue.Value, inst *cue.Instance, ref []string) {
 	name := b.ctx.makeRef(inst, ref)
 	b.addConjunct(func(b *builder) {
-		b.set("$ref", path.Join("#", b.ctx.refPrefix, name))
+		b.set("$ref", ast.NewString(path.Join("#", b.ctx.refPrefix, name)))
 	})
 
 	if b.ctx.inst != inst {
@@ -1123,7 +1138,8 @@
 	return path.Join(a...)
 }
 
-func (b *builder) int(v cue.Value) int64 {
+func (b *builder) int64(v cue.Value) int64 {
+	v, _ = v.Default()
 	i, err := v.Int64()
 	if err != nil {
 		b.failf(v, "could not retrieve int: %v", err)
@@ -1131,20 +1147,27 @@
 	return i
 }
 
-func (b *builder) decode(v cue.Value) interface{} {
-	var d interface{}
-	if err := v.Decode(&d); err != nil {
-		b.failf(v, "decode error: %v", err)
+func (b *builder) intExpr(i int64) ast.Expr {
+	return &ast.BasicLit{
+		Kind:  token.INT,
+		Value: fmt.Sprint(i),
 	}
-	return d
 }
 
-func (b *builder) big(v cue.Value) interface{} {
-	var mant big.Int
-	exp, err := v.MantExp(&mant)
-	if err != nil {
-		b.failf(v, "value not a number: %v", err)
-		return nil
-	}
-	return &decimal{apd.NewWithBigInt(&mant, int32(exp))}
+func (b *builder) int(v cue.Value) ast.Expr {
+	return b.intExpr(b.int64(v))
+}
+
+func (b *builder) inta(v cue.Value, offset int64) ast.Expr {
+	return b.intExpr(b.int64(v) + offset)
+}
+
+func (b *builder) decode(v cue.Value) ast.Expr {
+	v, _ = v.Default()
+	return v.Syntax().(ast.Expr)
+}
+
+func (b *builder) big(v cue.Value) ast.Expr {
+	v, _ = v.Default()
+	return v.Syntax().(ast.Expr)
 }
diff --git a/encoding/openapi/crd.go b/encoding/openapi/crd.go
index a6ed8a3..6825671 100644
--- a/encoding/openapi/crd.go
+++ b/encoding/openapi/crd.go
@@ -45,6 +45,7 @@
 
 import (
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
 )
 
 // newCoreBuilder returns a builder that represents a structural schema.
@@ -55,7 +56,7 @@
 }
 
 // coreSchema creates the core part of a structural OpenAPI.
-func (b *builder) coreSchema(name string) *oaSchema {
+func (b *builder) coreSchema(name string) *ast.StructLit {
 	oldPath := b.ctx.path
 	b.ctx.path = append(b.ctx.path, name)
 	defer func() { b.ctx.path = oldPath }()
@@ -74,11 +75,11 @@
 			sub := b.properties[k]
 			p.Set(k, sub.coreSchema(k))
 		}
-		if len(p.kvs) > 0 || b.items != nil {
+		if p.len() > 0 || b.items != nil {
 			b.setType("object", "")
 		}
-		if len(p.kvs) > 0 {
-			b.setSingle("properties", p, false)
+		if p.len() > 0 {
+			b.setSingle("properties", (*ast.StructLit)(p), false)
 		}
 		// TODO: in Structural schema only one of these is allowed.
 		if b.items != nil {
diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go
index a8692bf..9ec9239 100644
--- a/encoding/openapi/openapi.go
+++ b/encoding/openapi/openapi.go
@@ -18,13 +18,18 @@
 	"encoding/json"
 
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/token"
+	cuejson "cuelang.org/go/encoding/json"
 )
 
-// A Generator converts CUE to OpenAPI.
-type Generator struct {
+// A Config defines options for converting CUE to and from OpenAPI.
+type Config struct {
 	// Info specifies the info section of the OpenAPI document. To be a valid
 	// OpenAPI document, it must include at least the title and version fields.
-	Info OrderedMap
+	// Info may be a *ast.StructLit or any type that marshals to JSON.
+	Info interface{}
 
 	// ReferenceFunc allows users to specify an alternative representation
 	// for references. An empty string tells the generator to expand the type
@@ -55,8 +60,7 @@
 	ExpandReferences bool
 }
 
-// Config is now Generator
-type Config = Generator
+type Generator = Config
 
 // Gen generates the set OpenAPI schema for all top-level types of the
 // given instance.
@@ -71,25 +75,64 @@
 	return json.Marshal(all)
 }
 
-// All generates an OpenAPI definition from the given instance.
+// Generate generates the set of OpenAPI schema for all top-level types of the
+// given instance.
 //
 // Note: only a limited number of top-level types are supported so far.
-func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) {
-	all, err := g.Schemas(inst)
+func Generate(inst *cue.Instance, c *Config) (*ast.File, error) {
+	all, err := schemas(c, inst)
 	if err != nil {
 		return nil, err
 	}
+	top, err := c.compose(all)
+	if err != nil {
+		return nil, err
+	}
+	return &ast.File{Decls: top.Elts}, nil
+}
 
-	schemas := &OrderedMap{}
-	schemas.Set("schemas", all)
+// All generates an OpenAPI definition from the given instance.
+//
+// Note: only a limited number of top-level types are supported so far.
+// Deprecated: use Generate
+func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) {
+	all, err := schemas(g, inst)
+	if err != nil {
+		return nil, err
+	}
+	top, err := g.compose(all)
+	return (*OrderedMap)(top), err
+}
 
-	top := &OrderedMap{}
-	top.Set("openapi", "3.0.0")
-	top.Set("info", &g.Info)
-	top.Set("paths", &OrderedMap{})
-	top.Set("components", schemas)
+func (c *Config) compose(schemas *ast.StructLit) (*ast.StructLit, error) {
+	// 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:
+		info = x
+	case *OrderedMap:
+		info = (*ast.StructLit)(x)
+	case OrderedMap:
+		info = (*ast.StructLit)(&x)
+	default:
+		b, err := json.Marshal(x)
+		if err == nil {
+			info, err = cuejson.Extract("info", b)
+		}
+		if err != nil {
+			return nil, errors.Wrapf(err, token.NoPos,
+				"openapi: could not encode info section")
+		}
+	}
 
-	return top, nil
+	return ast.NewStruct(
+		"openapi", ast.NewString("3.0.0"),
+		"info", info,
+		"paths", ast.NewStruct(),
+		"components", ast.NewStruct("schemas", schemas),
+	), nil
 }
 
 // Schemas extracts component/schemas from the CUE top-level types.
@@ -98,7 +141,7 @@
 	if err != nil {
 		return nil, err
 	}
-	return comps, err
+	return (*OrderedMap)(comps), err
 }
 
 var defaultConfig = &Config{}
diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go
index fdfdd3f..336fac6 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -26,6 +26,7 @@
 	"github.com/kylelemons/godebug/diff"
 
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/errors"
 	"cuelang.org/go/cue/load"
 )
@@ -33,7 +34,10 @@
 var update *bool = flag.Bool("update", false, "update the test output")
 
 func TestParseDefinitions(t *testing.T) {
-	info := OrderedMap{kvs: []KeyValue{{"title", "test"}, {"version", "v1"}}}
+	info := *(*OrderedMap)(ast.NewStruct(
+		"title", ast.NewString("test"),
+		"version", ast.NewString("v1"),
+	))
 	defaultConfig := &Config{}
 	resolveRefs := &Config{Info: info, ExpandReferences: true}
 
diff --git a/encoding/openapi/orderedmap.go b/encoding/openapi/orderedmap.go
index 9e5113a..a0313ac 100644
--- a/encoding/openapi/orderedmap.go
+++ b/encoding/openapi/orderedmap.go
@@ -14,13 +14,18 @@
 
 package openapi
 
-import "encoding/json"
+import (
+	"cuelang.org/go/cue/ast"
+	"cuelang.org/go/internal/encoding/json"
+)
 
 // An OrderedMap is a set of key-value pairs that preserves the order in which
 // items were added. It marshals to JSON as an object.
-type OrderedMap struct {
-	kvs []KeyValue
-}
+//
+// Deprecated: the API now returns an ast.File. This allows OpenAPI to be
+// represented as JSON, YAML, or CUE data, in addition to being able to use
+// all the ast-related tooling.
+type OrderedMap ast.StructLit
 
 // KeyValue associates a value with a key.
 type KeyValue struct {
@@ -28,74 +33,65 @@
 	Value interface{}
 }
 
+func (m *OrderedMap) len() int {
+	return len(m.Elts)
+}
+
 // Pairs returns the KeyValue pairs associated with m.
 func (m *OrderedMap) Pairs() []KeyValue {
-	return m.kvs
+	kvs := make([]KeyValue, len(m.Elts))
+	for i, e := range m.Elts {
+		kvs[i].Key = label(e)
+		kvs[i].Value = e.(*ast.Field).Value
+	}
+	return kvs
+}
+
+func (m *OrderedMap) find(key string) *ast.Field {
+	for _, v := range m.Elts {
+		f, ok := v.(*ast.Field)
+		if !ok {
+			continue
+		}
+		s, _, err := ast.LabelName(f.Label)
+		if err == nil && s == key {
+			return f
+		}
+	}
+	return nil
 }
 
 // Set sets a key value pair. If a pair with the same key already existed, it
 // will be replaced with the new value. Otherwise, the new value is added to
 // the end.
-func (m *OrderedMap) Set(key string, value interface{}) {
-	for i, v := range m.kvs {
-		if v.Key == key {
-			m.kvs[i].Value = value
-			return
-		}
+func (m *OrderedMap) Set(key string, expr ast.Expr) {
+	if f := m.find(key); f != nil {
+		f.Value = expr
+		return
 	}
-	m.kvs = append(m.kvs, KeyValue{key, value})
-}
-
-// SetAll replaces existing key-value pairs with the given ones. The keys must
-// be unique.
-func (m *OrderedMap) SetAll(kvs []KeyValue) {
-	m.kvs = kvs
+	m.Elts = append(m.Elts, &ast.Field{
+		Label: ast.NewString(key),
+		Value: expr,
+	})
 }
 
 // exists reports whether a key-value pair exists for the given key.
 func (m *OrderedMap) exists(key string) bool {
-	for _, v := range m.kvs {
-		if v.Key == key {
-			return true
-		}
-	}
-	return false
+	return m.find(key) != nil
 }
 
 // exists reports whether a key-value pair exists for the given key.
 func (m *OrderedMap) getMap(key string) *OrderedMap {
-	for _, v := range m.kvs {
-		if v.Key == key {
-			return v.Value.(*OrderedMap)
-		}
+	f := m.find(key)
+	if f == nil {
+		return nil
 	}
-	return nil
+	return (*OrderedMap)(f.Value.(*ast.StructLit))
 }
 
 // MarshalJSON implements json.Marshaler.
 func (m *OrderedMap) MarshalJSON() (b []byte, err error) {
 	// This is a pointer receiever to enforce that we only store pointers to
 	// OrderedMap in the output.
-
-	b = append(b, '{')
-	for i, v := range m.kvs {
-		if i > 0 {
-			b = append(b, ",\n"...)
-		}
-		key, ferr := json.Marshal(v.Key)
-		if je, ok := ferr.(*json.MarshalerError); ok {
-			return nil, je.Err
-		}
-		b = append(b, key...)
-		b = append(b, ": "...)
-
-		value, jerr := json.Marshal(v.Value)
-		if je, ok := jerr.(*json.MarshalerError); ok {
-			err = jerr
-			value, _ = json.Marshal(je.Err.Error())
-		}
-		b = append(b, value...)
-	}
-	b = append(b, '}')
-	return b, err
+	return json.Encode((*ast.StructLit)(m))
 }
diff --git a/encoding/openapi/types.go b/encoding/openapi/types.go
index 9e2b07d..3a82362 100644
--- a/encoding/openapi/types.go
+++ b/encoding/openapi/types.go
@@ -20,7 +20,10 @@
 	"github.com/cockroachdb/apd/v2"
 
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/ast"
 	"cuelang.org/go/cue/format"
+	"cuelang.org/go/cue/literal"
+	"cuelang.org/go/cue/token"
 )
 
 // See https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#format
@@ -69,7 +72,7 @@
 	return r
 }
 
-func simplify(b *builder, t *OrderedMap) {
+func simplify(b *builder, t *ast.StructLit) {
 	if b.format == "" {
 		return
 	}
@@ -79,62 +82,62 @@
 	}
 }
 
-func simplifyNumber(t *OrderedMap, format string) string {
-	pairs := t.kvs
+func simplifyNumber(t *ast.StructLit, format string) string {
+	fields := t.Elts
 	k := 0
-	for i, kv := range pairs {
-		switch kv.Key {
+	for i, d := range fields {
+		switch label(d) {
 		case "minimum":
-			if decimalEqual(minMap[format], kv.Value) {
+			if decimalEqual(minMap[format], value(d)) {
 				continue
 			}
 		case "maximum":
-			if decimalEqual(maxMap[format], kv.Value) {
+			if decimalEqual(maxMap[format], value(d)) {
 				continue
 			}
 		}
-		pairs[k] = pairs[i]
+		fields[k] = fields[i]
 		k++
 	}
-	t.kvs = pairs[:k]
+	t.Elts = fields[:k]
 	return format
 }
 
-func decimalEqual(d *decimal, v interface{}) bool {
+func decimalEqual(d *apd.Decimal, v ast.Expr) bool {
 	if d == nil {
 		return false
 	}
-	b, ok := v.(*decimal)
-	if !ok {
+	lit, ok := v.(*ast.BasicLit)
+	if !ok || (lit.Kind != token.INT && lit.Kind != token.FLOAT) {
 		return false
 	}
-	return d.Cmp(b.Decimal) == 0
+	n := literal.NumInfo{}
+	if literal.ParseNum(lit.Value, &n) != nil {
+		return false
+	}
+	var b apd.Decimal
+	if n.Decimal(&b) != nil {
+		return false
+	}
+	return d.Cmp(&b) == 0
 }
 
-type decimal struct {
-	*apd.Decimal
-}
-
-func (d *decimal) MarshalJSON() (b []byte, err error) {
-	return d.MarshalText()
-}
-
-func mustDecimal(s string) *decimal {
+func mustDecimal(s string) *apd.Decimal {
 	d, _, err := apd.NewFromString(s)
 	if err != nil {
 		panic(err)
 	}
-	return &decimal{d}
+	return d
 }
 
 var (
-	minMap = map[string]*decimal{
+	minMap = map[string]*apd.Decimal{
 		"int32":  mustDecimal("-2147483648"),
 		"int64":  mustDecimal("-9223372036854775808"),
 		"float":  mustDecimal("-3.40282346638528859811704183484516925440e+38"),
 		"double": mustDecimal("-1.797693134862315708145274237317043567981e+308"),
 	}
-	maxMap = map[string]*decimal{
+	maxMap = map[string]*apd.Decimal{
 		"int32":  mustDecimal("2147483647"),
 		"int64":  mustDecimal("9223372036854775807"),
 		"float":  mustDecimal("+3.40282346638528859811704183484516925440e+38"),