encoding/openapi: make OrderedMap type opaque

The OrderedMap needs to be a pointer, which may be
a bit unintuitive it being a slice. Change it to as struct.

Info is left as a non-pointer to indicate the non-optional
nature of it. The user of pointers is enforced by making
MarshalJSON only work for pointer types.

Change-Id: I8481dd8974825928815bfef3acd1eb5fc0274029
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2460
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index 9adfbe6..9b49b1f 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -56,7 +56,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 *OrderedMap, err error) {
 	var fieldFilter *regexp.Regexp
 	if g.FieldFilter != "" {
 		fieldFilter, err = regexp.Compile(g.FieldFilter)
@@ -107,7 +107,7 @@
 		if c.isInternal(label) {
 			continue
 		}
-		c.schemas.set(c.makeRef(inst, []string{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.
@@ -123,11 +123,11 @@
 
 		for _, k := range external {
 			ext := c.externalRefs[k]
-			c.schemas.set(ext.ref, c.build(ext.ref, ext.value.Eval()))
+			c.schemas.Set(ext.ref, c.build(ext.ref, ext.value.Eval()))
 		}
 	}
 
-	return *c.schemas, nil
+	return c.schemas, nil
 }
 
 func (c *buildContext) build(name string, v cue.Value) *oaSchema {
@@ -459,11 +459,11 @@
 		b.setFilter("Schema", "required", required)
 	}
 
-	properties := map[string]*oaSchema{}
+	properties := &OrderedMap{}
 	for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); {
-		properties[i.Label()] = b.schema(i.Label(), i.Value())
+		properties.Set(i.Label(), b.schema(i.Label(), i.Value()))
 	}
-	if len(properties) > 0 {
+	if len(properties.kvs) > 0 {
 		b.set("properties", properties)
 	}
 
@@ -738,9 +738,9 @@
 
 func setType(t *oaSchema, b *builder) {
 	if b.typ != "" {
-		t.set("type", b.typ)
+		t.Set("type", b.typ)
 		if b.format != "" {
-			t.set("format", b.format)
+			t.Set("format", b.format)
 		}
 	}
 }
@@ -762,19 +762,19 @@
 		b.current = &OrderedMap{}
 		b.allOf = append(b.allOf, b.current)
 	}
-	b.current.set(key, v)
+	b.current.Set(key, v)
 }
 
 func (b *builder) kv(key string, value interface{}) *oaSchema {
 	constraint := &OrderedMap{}
 	setType(constraint, b)
-	constraint.set(key, value)
+	constraint.Set(key, value)
 	return constraint
 }
 
 func (b *builder) setNot(key string, value interface{}) {
 	not := &OrderedMap{}
-	not.set("not", b.kv(key, value))
+	not.Set("not", b.kv(key, value))
 	b.add(not)
 }
 
@@ -795,7 +795,7 @@
 
 	default:
 		t := &OrderedMap{}
-		t.set("allOf", b.allOf)
+		t.Set("allOf", b.allOf)
 		return t
 	}
 }
diff --git a/encoding/openapi/openapi.go b/encoding/openapi/openapi.go
index 58cb41f..74cf693 100644
--- a/encoding/openapi/openapi.go
+++ b/encoding/openapi/openapi.go
@@ -75,25 +75,25 @@
 // All generates an OpenAPI definition from 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) {
+func (g *Generator) All(inst *cue.Instance) (*OrderedMap, error) {
 	all, err := g.Schemas(inst)
 	if err != nil {
 		return nil, err
 	}
 
 	schemas := &OrderedMap{}
-	schemas.set("schema", all)
+	schemas.Set("schema", all)
 
-	top := OrderedMap{}
-	top.set("openapi", "3.0.0")
-	top.set("info", g.Info)
-	top.set("components", schemas)
+	top := &OrderedMap{}
+	top.Set("openapi", "3.0.0")
+	top.Set("info", &g.Info)
+	top.Set("components", schemas)
 
 	return top, nil
 }
 
 // Schemas extracts component/schemas from the CUE top-level types.
-func (g *Generator) Schemas(inst *cue.Instance) (OrderedMap, error) {
+func (g *Generator) Schemas(inst *cue.Instance) (*OrderedMap, error) {
 	comps, err := schemas(g, inst)
 	if err != nil {
 		return nil, err
diff --git a/encoding/openapi/openapi_test.go b/encoding/openapi/openapi_test.go
index 7bb1b0c..d5eb721 100644
--- a/encoding/openapi/openapi_test.go
+++ b/encoding/openapi/openapi_test.go
@@ -31,7 +31,7 @@
 var update *bool = flag.Bool("update", false, "update the test output")
 
 func TestParseDefinitions(t *testing.T) {
-	info := OrderedMap{KeyValue{"title", "test"}, KeyValue{"version", "v1"}}
+	info := OrderedMap{kvs: []KeyValue{{"title", "test"}, {"version", "v1"}}}
 	defaultConfig := &Config{}
 	resolveRefs := &Config{Info: info, ExpandReferences: true}
 
diff --git a/encoding/openapi/orderedmap.go b/encoding/openapi/orderedmap.go
index 8d419a8..91a2ec4 100644
--- a/encoding/openapi/orderedmap.go
+++ b/encoding/openapi/orderedmap.go
@@ -18,7 +18,9 @@
 
 // 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 []KeyValue
+type OrderedMap struct {
+	kvs []KeyValue
+}
 
 // KeyValue associates a value with a key.
 type KeyValue struct {
@@ -26,26 +28,37 @@
 	Value interface{}
 }
 
-func (m *OrderedMap) prepend(key string, value interface{}) {
-	*m = append([]KeyValue{{key, value}}, (*m)...)
+// Pairs returns the KeyValue pairs associated with m.
+func (m *OrderedMap) Pairs() []KeyValue {
+	return m.kvs
 }
 
-// set sets a key value pair. If a pair with the same key already existed, it
+func (m *OrderedMap) prepend(key string, value interface{}) {
+	m.kvs = append([]KeyValue{{key, value}}, m.kvs...)
+}
+
+// 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 {
+func (m *OrderedMap) Set(key string, value interface{}) {
+	for i, v := range m.kvs {
 		if v.Key == key {
-			(*m)[i].Value = value
+			m.kvs[i].Value = value
 			return
 		}
 	}
-	*m = append(*m, KeyValue{key, value})
+	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
 }
 
 // exists reports whether a key-value pair exists for the given key.
-func (m OrderedMap) exists(key string) bool {
-	for _, v := range m {
+func (m *OrderedMap) exists(key string) bool {
+	for _, v := range m.kvs {
 		if v.Key == key {
 			return true
 		}
@@ -53,10 +66,13 @@
 	return false
 }
 
-// MarshalJSON implements Marshal
-func (m OrderedMap) MarshalJSON() (b []byte, err error) {
+// 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 {
+	for i, v := range m.kvs {
 		if i > 0 {
 			b = append(b, ",\n"...)
 		}
diff --git a/encoding/openapi/testdata/oneof-funcs.json b/encoding/openapi/testdata/oneof-funcs.json
index 2ca742f..1510628 100644
--- a/encoding/openapi/testdata/oneof-funcs.json
+++ b/encoding/openapi/testdata/oneof-funcs.json
@@ -50,8 +50,8 @@
                "count"
             ],
             "properties": {
-               "count": {
-                  "$ref": "#/components/schema/MYINT"
+               "include": {
+                  "$ref": "#/components/schema/MYSTRING"
                },
                "exclude": {
                   "description": "Randomly picked description from a set of size one.",
@@ -60,8 +60,8 @@
                      "$ref": "#/components/schema/MYSTRING"
                   }
                },
-               "include": {
-                  "$ref": "#/components/schema/MYSTRING"
+               "count": {
+                  "$ref": "#/components/schema/MYINT"
                }
             }
          }
diff --git a/encoding/openapi/testdata/oneof.json b/encoding/openapi/testdata/oneof.json
index 83384fb..f37e664 100644
--- a/encoding/openapi/testdata/oneof.json
+++ b/encoding/openapi/testdata/oneof.json
@@ -42,8 +42,8 @@
                "count"
             ],
             "properties": {
-               "count": {
-                  "$ref": "#/components/schema/MyInt"
+               "include": {
+                  "$ref": "#/components/schema/MyString"
                },
                "exclude": {
                   "type": "array",
@@ -51,8 +51,8 @@
                      "$ref": "#/components/schema/MyString"
                   }
                },
-               "include": {
-                  "$ref": "#/components/schema/MyString"
+               "count": {
+                  "$ref": "#/components/schema/MyInt"
                }
             }
          }
diff --git a/encoding/openapi/testdata/openapi-norefs.json b/encoding/openapi/testdata/openapi-norefs.json
index d6f80b3..425130b 100644
--- a/encoding/openapi/testdata/openapi-norefs.json
+++ b/encoding/openapi/testdata/openapi-norefs.json
@@ -16,18 +16,6 @@
                      "bar"
                   ],
                   "properties": {
-                     "bar": {
-                        "type": "array",
-                        "items": {
-                           "type": "string",
-                           "format": "string"
-                        }
-                     },
-                     "foo": {
-                        "type": "number",
-                        "exclusiveMinimum": 10,
-                        "exclusiveMaximum": 1000
-                     },
                      "port": {
                         "type": "object",
                         "required": [
@@ -35,16 +23,28 @@
                            "obj"
                         ],
                         "properties": {
+                           "port": {
+                              "type": "integer"
+                           },
                            "obj": {
                               "type": "array",
                               "items": {
                                  "type": "integer"
                               }
-                           },
-                           "port": {
-                              "type": "integer"
                            }
                         }
+                     },
+                     "foo": {
+                        "type": "number",
+                        "exclusiveMinimum": 10,
+                        "exclusiveMaximum": 1000
+                     },
+                     "bar": {
+                        "type": "array",
+                        "items": {
+                           "type": "string",
+                           "format": "string"
+                        }
                      }
                   }
                },
@@ -89,14 +89,14 @@
                "obj"
             ],
             "properties": {
+               "port": {
+                  "type": "integer"
+               },
                "obj": {
                   "type": "array",
                   "items": {
                      "type": "integer"
                   }
-               },
-               "port": {
-                  "type": "integer"
                }
             }
          },
@@ -277,14 +277,14 @@
                            "obj"
                         ],
                         "properties": {
+                           "port": {
+                              "type": "integer"
+                           },
                            "obj": {
                               "type": "array",
                               "items": {
                                  "type": "integer"
                               }
-                           },
-                           "port": {
-                              "type": "integer"
                            }
                         }
                      },
diff --git a/encoding/openapi/testdata/openapi.json b/encoding/openapi/testdata/openapi.json
index d4c6691..d64d270 100644
--- a/encoding/openapi/testdata/openapi.json
+++ b/encoding/openapi/testdata/openapi.json
@@ -13,12 +13,9 @@
                      "bar"
                   ],
                   "properties": {
-                     "bar": {
-                        "type": "array",
-                        "items": {
-                           "type": "string",
-                           "format": "string"
-                        }
+                     "port": {
+                        "$ref": "#/components/schema/Port",
+                        "type": "object"
                      },
                      "foo": {
                         "allOf": [
@@ -32,9 +29,12 @@
                            }
                         ]
                      },
-                     "port": {
-                        "$ref": "#/components/schema/Port",
-                        "type": "object"
+                     "bar": {
+                        "type": "array",
+                        "items": {
+                           "type": "string",
+                           "format": "string"
+                        }
                      }
                   }
                },
@@ -79,14 +79,14 @@
                "obj"
             ],
             "properties": {
+               "port": {
+                  "type": "integer"
+               },
                "obj": {
                   "type": "array",
                   "items": {
                      "type": "integer"
                   }
-               },
-               "port": {
-                  "type": "integer"
                }
             }
          },
diff --git a/encoding/openapi/testdata/simple-filter.json b/encoding/openapi/testdata/simple-filter.json
index a971c73..8760bcd 100644
--- a/encoding/openapi/testdata/simple-filter.json
+++ b/encoding/openapi/testdata/simple-filter.json
@@ -15,20 +15,20 @@
                "double"
             ],
             "properties": {
-               "double": {
-                  "type": "number",
-                  "format": "double"
-               },
-               "float": {
-                  "type": "number",
-                  "format": "float"
-               },
                "mediumNum": {
                   "type": "integer",
                   "format": "int32"
                },
                "smallNum": {
                   "type": "integer"
+               },
+               "float": {
+                  "type": "number",
+                  "format": "float"
+               },
+               "double": {
+                  "type": "number",
+                  "format": "double"
                }
             }
          }
diff --git a/encoding/openapi/testdata/simple.json b/encoding/openapi/testdata/simple.json
index ce7675e..ee9aa52 100644
--- a/encoding/openapi/testdata/simple.json
+++ b/encoding/openapi/testdata/simple.json
@@ -15,14 +15,6 @@
                "double"
             ],
             "properties": {
-               "double": {
-                  "type": "number",
-                  "format": "double"
-               },
-               "float": {
-                  "type": "number",
-                  "format": "float"
-               },
                "mediumNum": {
                   "type": "integer",
                   "format": "int32"
@@ -31,6 +23,14 @@
                   "type": "integer",
                   "minimum": -128,
                   "maximum": 127
+               },
+               "float": {
+                  "type": "number",
+                  "format": "float"
+               },
+               "double": {
+                  "type": "number",
+                  "format": "double"
                }
             }
          }
diff --git a/encoding/openapi/types.go b/encoding/openapi/types.go
index 5cf481d..3995055 100644
--- a/encoding/openapi/types.go
+++ b/encoding/openapi/types.go
@@ -59,7 +59,7 @@
 }
 
 func simplifyNumber(t *OrderedMap, format string) string {
-	pairs := *t
+	pairs := t.kvs
 	k := 0
 	for i, kv := range pairs {
 		switch kv.Key {
@@ -75,7 +75,7 @@
 		pairs[i] = pairs[k]
 		k++
 	}
-	*t = pairs[:k]
+	t.kvs = pairs[:k]
 	return format
 }