encoding/openapi: record path in error

The path is added to a new openapiError type
that implements cue/errors.Error.
Even though failf panics, usage of failf is followed
by a return make clear that the function exits,
if necessary.

Change-Id: I2c8ab2ab7ff0c41221e519da48feb1b598c3a472
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2348
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
diff --git a/encoding/openapi/build.go b/encoding/openapi/build.go
index 0cef528..8d64cee 100644
--- a/encoding/openapi/build.go
+++ b/encoding/openapi/build.go
@@ -18,9 +18,11 @@
 	"fmt"
 	"math"
 	"path"
+	"strconv"
 	"strings"
 
 	"cuelang.org/go/cue"
+	"cuelang.org/go/cue/errors"
 )
 
 type buildContext struct {
@@ -44,8 +46,14 @@
 	}
 
 	defer func() {
+		switch x := recover().(type) {
+		case nil:
+		case *openapiError:
+			err = x
+		default:
+			panic(x)
+		}
 		if x := recover(); x != nil {
-			// TODO: check if it's one of our own.
 			path := strings.Join(c.path, ".")
 			err = fmt.Errorf("error: %s: %v", path, x)
 		}
@@ -63,16 +71,34 @@
 	}
 	for i.Next() {
 		// message, enum, or constant.
-		c.schemas.Set(i.Label(), c.build(i.Value()))
+		label := i.Label()
+		c.schemas.Set(label, c.build(label, i.Value()))
 	}
 	return comps, nil
 }
 
-func (c *buildContext) build(v cue.Value) *oaSchema {
-	return newRootBuilder(c).schema(v)
+func (c *buildContext) build(name string, v cue.Value) *oaSchema {
+	return newRootBuilder(c).schema(name, v)
 }
 
-func (b *builder) schema(v cue.Value) *oaSchema {
+// shouldExpand reports is the given identifier is not exported.
+func (c *buildContext) shouldExpand(name string) bool {
+	return c.expandRefs
+}
+
+func (b *builder) failf(v cue.Value, format string, args ...interface{}) {
+	panic(&openapiError{
+		errors.NewMessage(format, args),
+		b.ctx.path,
+		v.Pos(),
+	})
+}
+
+func (b *builder) schema(name string, v cue.Value) *oaSchema {
+	oldPath := b.ctx.path
+	b.ctx.path = append(b.ctx.path, name)
+	defer func() { b.ctx.path = oldPath }()
+
 	c := newRootBuilder(b.ctx)
 	c.value(v, nil)
 	schema := c.finish()
@@ -90,7 +116,7 @@
 func (b *builder) value(v cue.Value, f typeFunc) {
 	count := 0
 	var values cue.Value
-	if b.ctx.expandRefs {
+	if b.ctx.shouldExpand(strings.Join(v.Reference(), ".")) {
 		values = v
 		count = 1
 	} else {
@@ -122,7 +148,7 @@
 			switch {
 			case isConcrete(v):
 				b.dispatch(f, v)
-				b.set("enum", []interface{}{decode(v)})
+				b.set("enum", []interface{}{b.decode(v)})
 
 			default:
 				if a := appendSplit(nil, cue.OrOp, v); len(a) > 1 {
@@ -130,7 +156,8 @@
 				} else {
 					v = a[0]
 					if err := v.Err(); err != nil {
-						panic(err)
+						b.failf(v, "openapi: %v", err)
+						return
 					}
 					b.dispatch(f, v)
 				}
@@ -213,7 +240,7 @@
 			nullable = true
 
 		case isConcrete(v):
-			enums = append(enums, decode(v))
+			enums = append(enums, b.decode(v))
 
 		default:
 			disjuncts = append(disjuncts, v)
@@ -332,14 +359,14 @@
 
 	properties := map[string]*oaSchema{}
 	for i, _ := v.Fields(cue.Optional(true), cue.Hidden(false)); i.Next(); {
-		properties[i.Label()] = b.schema(i.Value())
+		properties[i.Label()] = b.schema(i.Label(), i.Value())
 	}
 	if len(properties) > 0 {
 		b.set("properties", properties)
 	}
 
 	if t, ok := v.Elem(); ok {
-		b.set("additionalProperties", b.schema(t))
+		b.set("additionalProperties", b.schema("*", t))
 	}
 
 	// TODO: maxProperties, minProperties: can be done once we allow cap to
@@ -376,8 +403,9 @@
 	// There is never a need for allOf or anyOf. Note that a CUE list
 	// corresponds almost one-to-one to OpenAPI lists.
 	items := []*oaSchema{}
-	for i, _ := v.List(); i.Next(); {
-		items = append(items, b.schema(i.Value()))
+	count := 0
+	for i, _ := v.List(); i.Next(); count++ {
+		items = append(items, b.schema(strconv.Itoa(count), i.Value()))
 	}
 	if len(items) > 0 {
 		// TODO: per-item schema are not allowed in OpenAPI, only in JSON Schema.
@@ -403,7 +431,7 @@
 
 	if !hasMax || int64(len(items)) < maxLength {
 		if typ, ok := v.Elem(); ok {
-			t := b.schema(typ)
+			t := b.schema("*", typ)
 			if len(items) > 0 {
 				b.set("additionalItems", t)
 			} else {
@@ -435,7 +463,8 @@
 		})
 
 	default:
-		panic(fmt.Sprintf("unsupported op for list capacity %v", op))
+		b.failf(v, "unsupported op for list capacity %v", op)
+		return
 	}
 }
 
@@ -527,7 +556,8 @@
 			// TODO: this may be an unresolved interpolation or expression. Consider
 			// whether it is reasonable to treat unevaluated operands as wholes and
 			// generate a compound regular expression.
-			panic(err)
+			b.failf(v, "regexp value must be a string: %v", err)
+			return
 		}
 		if op == cue.RegexMatchOp {
 			b.set("pattern", s)
@@ -543,7 +573,7 @@
 		// TODO: determine formats from specific types.
 
 	default:
-		panic(fmt.Sprintf("unsupported of %v for bytes type", op))
+		b.failf(v, "unsupported op %v for string type", op)
 	}
 }
 
@@ -556,7 +586,8 @@
 			// TODO: this may be an unresolved interpolation or expression. Consider
 			// whether it is reasonable to treat unevaluated operands as wholes and
 			// generate a compound regular expression.
-			panic(err)
+			b.failf(v, "regexp value must be of type bytes: %v", err)
+			return
 		}
 
 		if op == cue.RegexMatchOp {
@@ -572,7 +603,7 @@
 	case cue.NoOp:
 
 	default:
-		panic(fmt.Sprintf("unsupported of %v for bytes type", op))
+		b.failf(v, "unsupported op %v for bytes type", op)
 	}
 }
 
@@ -643,7 +674,8 @@
 	switch len(b.allOf) {
 	case 0:
 		if b.typ == "" {
-			panic("no type specified at finish")
+			b.failf(cue.Value{}, "no type specified at finish")
+			return nil
 		}
 		t := &orderedMap{}
 		setType(t, b)
@@ -682,15 +714,15 @@
 func (b *builder) int(v cue.Value) int64 {
 	i, err := v.Int64()
 	if err != nil {
-		panic("could not retrieve int")
+		b.failf(v, "could not retrieve int: %v", err)
 	}
 	return i
 }
 
-func decode(v cue.Value) interface{} {
+func (b *builder) decode(v cue.Value) interface{} {
 	var d interface{}
 	if err := v.Decode(&d); err != nil {
-		panic(err)
+		b.failf(v, "decode error: %v", err)
 	}
 	return d
 }
diff --git a/encoding/openapi/errors.go b/encoding/openapi/errors.go
new file mode 100644
index 0000000..6201613
--- /dev/null
+++ b/encoding/openapi/errors.go
@@ -0,0 +1,41 @@
+// Copyright 2019 CUE Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package openapi
+
+import (
+	"cuelang.org/go/cue/errors"
+	"cuelang.org/go/cue/token"
+)
+
+var _ errors.Error = &openapiError{}
+
+// implements cue/Error
+type openapiError struct {
+	errors.Message
+	path []string
+	pos  token.Pos
+}
+
+func (e *openapiError) Position() token.Pos {
+	return e.pos
+}
+
+func (e *openapiError) InputPositions() []token.Pos {
+	return nil
+}
+
+func (e *openapiError) Path() []string {
+	return e.path
+}