cue: add appendPath for creating paths
Part of larger error improvements.
Issue #52
Change-Id: I287b69f05350929a5655727a8015bc994a7fddb4
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/2201
Reviewed-by: Marcel van Lohuizen <mpvl@google.com>
diff --git a/cue/types.go b/cue/types.go
index 75637da..810753c 100644
--- a/cue/types.go
+++ b/cue/types.go
@@ -23,6 +23,7 @@
"math/big"
"strconv"
"strings"
+ "unicode"
"cuelang.org/go/cue/ast"
"cuelang.org/go/cue/errors"
@@ -474,6 +475,47 @@
arc
}
+// path returns the path of the value.
+func (v *valueData) appendPath(a []string, idx *index) ([]string, kind) {
+ var k kind
+ if v.parent != nil {
+ a, k = v.parent.appendPath(a, idx)
+ }
+ switch k {
+ case listKind:
+ a = append(a, strconv.FormatInt(int64(v.index), 10))
+ case structKind:
+ f := idx.labelStr(v.arc.feature)
+ if !isIdent(f) && !isNumber(f) {
+ f = quote(f, '"')
+ }
+ a = append(a, f)
+ }
+ return a, v.arc.cache.kind()
+}
+
+var validIdent = []*unicode.RangeTable{unicode.L, unicode.N}
+
+func isIdent(s string) bool {
+ valid := []*unicode.RangeTable{unicode.Letter}
+ for _, r := range s {
+ if !unicode.In(r, valid...) && r != '_' {
+ return false
+ }
+ valid = validIdent
+ }
+ return true
+}
+
+func isNumber(s string) bool {
+ for _, r := range s {
+ if r < '0' || '9' < r {
+ return false
+ }
+ }
+ return true
+}
+
// Value holds any value, which may be a Boolean, Error, List, Null, Number,
// Struct, or String.
type Value struct {
diff --git a/cue/types_test.go b/cue/types_test.go
index 4df1cc3..5e6ed27 100644
--- a/cue/types_test.go
+++ b/cue/types_test.go
@@ -21,6 +21,7 @@
"math"
"math/big"
"reflect"
+ "strconv"
"strings"
"testing"
@@ -1054,6 +1055,65 @@
}
}
+func TestPath(t *testing.T) {
+ config := `
+ a b c: 5
+ b: {
+ b1: 3
+ b2: 4
+ "b 3": 5
+ "4b": 6
+ l: [
+ {a: 2},
+ {c: 2},
+ ]
+ }
+ `
+ mkpath := func(p ...string) []string { return p }
+ testCases := [][]string{
+ mkpath("a", "b", "c"),
+ mkpath("b", "l", "1", "c"),
+ mkpath("b", `"b 3"`),
+ mkpath("b", `"4b"`),
+ }
+ for _, tc := range testCases {
+ r := Runtime{}
+ inst, err := r.Parse("config", config)
+ if err != nil {
+ t.Fatal(err)
+ }
+ t.Run(strings.Join(tc, "."), func(t *testing.T) {
+ v := inst.Lookup(tc[0])
+ for _, e := range tc[1:] {
+ if '0' <= e[0] && e[0] <= '9' {
+ i, err := strconv.Atoi(e)
+ if err != nil {
+ t.Fatal(err)
+ }
+ iter, err := v.List()
+ if err != nil {
+ t.Fatal(err)
+ }
+ for c := 0; iter.Next(); c++ {
+ if c == i {
+ v = iter.Value()
+ break
+ }
+ }
+ } else if e[0] == '"' {
+ v = v.Lookup(e[1 : len(e)-1])
+ } else {
+ v = v.Lookup(e)
+ }
+ }
+ got, _ := v.path.appendPath(nil, v.idx)
+ if !reflect.DeepEqual(got, tc) {
+ t.Errorf("got %v; want %v", got, tc)
+ }
+ })
+ }
+}
+
func TestValueLookup(t *testing.T) {
config := `
a: {