Skip to content

Commit 409dacf

Browse files
committed
cue: support getting and looking up paths
Goals of this API: - minimize possibility for error when converting strings to either identifiers or quoted string - extendablity to allow CUE's planned query extensions: - allow `[]` - allow foo? for looking up templates and element types - filters - value yielding - etc. For the reviewer: should put Path be an opaque type or []Selection. Change-Id: I4fa150b4c353b5dfac20c773a727a1964fc81c31 Reviewed-on: https://cue-review.googlesource.com/c/cue/+/7441 Reviewed-by: CUE cueckoo <[email protected]> Reviewed-by: Marcel van Lohuizen <[email protected]>
1 parent 1a2105e commit 409dacf

File tree

3 files changed

+484
-0
lines changed

3 files changed

+484
-0
lines changed

cue/path.go

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
// Copyright 2020 CUE Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cue
16+
17+
import (
18+
"strconv"
19+
"strings"
20+
21+
"cuelang.org/go/cue/ast"
22+
"cuelang.org/go/cue/errors"
23+
"cuelang.org/go/cue/literal"
24+
"cuelang.org/go/cue/parser"
25+
"cuelang.org/go/cue/token"
26+
"cuelang.org/go/internal"
27+
"cuelang.org/go/internal/core/adt"
28+
"github.com/cockroachdb/apd/v2"
29+
)
30+
31+
// A Selector is path component in a path.
32+
type Selector struct {
33+
sel selector
34+
}
35+
36+
// String reports the CUE representation of a selector.
37+
func (sel Selector) String() string {
38+
return sel.sel.String()
39+
}
40+
41+
type selector interface {
42+
String() string
43+
44+
feature(ctx adt.Runtime) adt.Feature
45+
kind() adt.FeatureType
46+
}
47+
48+
// A Path is series of selectors to query a CUE value.
49+
type Path struct {
50+
path []Selector
51+
}
52+
53+
// MakePath creates a Path from a sequence of selectors.
54+
func MakePath(selectors ...Selector) Path {
55+
return Path{path: selectors}
56+
}
57+
58+
// ParsePath parses a CUE expression into a Path. Any error resulting from
59+
// this conversion can be obtained by calling Err on the result.
60+
func ParsePath(s string) Path {
61+
expr, err := parser.ParseExpr("", s)
62+
if err != nil {
63+
return MakePath(Selector{pathError{errors.Promote(err, "invalid path")}})
64+
}
65+
66+
return Path{path: toSelectors(expr)}
67+
}
68+
69+
// String reports the CUE representation of p.
70+
func (p Path) String() string {
71+
if err := p.Err(); err != nil {
72+
return "_|_"
73+
}
74+
75+
b := &strings.Builder{}
76+
for i, sel := range p.path {
77+
x := sel.sel
78+
// TODO: use '.' in all cases, once supported.
79+
switch {
80+
case x.kind() == adt.IntLabel:
81+
b.WriteByte('[')
82+
b.WriteString(x.String())
83+
b.WriteByte(']')
84+
continue
85+
case i > 0:
86+
b.WriteByte('.')
87+
}
88+
89+
b.WriteString(x.String())
90+
}
91+
return b.String()
92+
}
93+
94+
func toSelectors(expr ast.Expr) []Selector {
95+
switch x := expr.(type) {
96+
case *ast.Ident:
97+
return []Selector{identSelector(x)}
98+
99+
case *ast.IndexExpr:
100+
a := toSelectors(x.X)
101+
var sel Selector
102+
if b, ok := x.Index.(*ast.BasicLit); !ok {
103+
sel = Selector{pathError{
104+
errors.Newf(token.NoPos, "non-constant expression %s",
105+
internal.DebugStr(x.Index))}}
106+
} else {
107+
sel = basicLitSelector(b)
108+
}
109+
return append(a, sel)
110+
111+
case *ast.SelectorExpr:
112+
a := toSelectors(x.X)
113+
return append(a, identSelector(x.Sel))
114+
115+
default:
116+
return []Selector{Selector{pathError{
117+
errors.Newf(token.NoPos, "invalid label %s ", internal.DebugStr(x)),
118+
}}}
119+
}
120+
}
121+
122+
func basicLitSelector(b *ast.BasicLit) Selector {
123+
switch b.Kind {
124+
case token.INT:
125+
var n literal.NumInfo
126+
if err := literal.ParseNum(b.Value, &n); err != nil {
127+
return Selector{pathError{
128+
errors.Newf(token.NoPos, "invalid string index %s", b.Value),
129+
}}
130+
}
131+
var d apd.Decimal
132+
_ = n.Decimal(&d)
133+
i, err := d.Int64()
134+
if err != nil {
135+
return Selector{pathError{
136+
errors.Newf(token.NoPos, "integer %s out of range", b.Value),
137+
}}
138+
}
139+
return Index(int(i))
140+
141+
case token.STRING:
142+
info, _, _, _ := literal.ParseQuotes(b.Value, b.Value)
143+
if !info.IsDouble() {
144+
return Selector{pathError{
145+
errors.Newf(token.NoPos, "invalid string index %s", b.Value)}}
146+
}
147+
s, _ := literal.Unquote(b.Value)
148+
return Selector{stringSelector(s)}
149+
150+
default:
151+
return Selector{pathError{
152+
errors.Newf(token.NoPos, "invalid literal %s", b.Value),
153+
}}
154+
}
155+
}
156+
157+
func identSelector(label ast.Label) Selector {
158+
switch x := label.(type) {
159+
case *ast.Ident:
160+
if isHiddenOrDefinition(x.Name) {
161+
return Selector{definitionSelector(x.Name)}
162+
}
163+
return Selector{stringSelector(x.Name)}
164+
165+
case *ast.BasicLit:
166+
return basicLitSelector(x)
167+
168+
default:
169+
return Selector{pathError{
170+
errors.Newf(token.NoPos, "invalid label %s ", internal.DebugStr(x)),
171+
}}
172+
}
173+
}
174+
175+
// Err reports errors that occurred when generating the path.
176+
func (p Path) Err() error {
177+
var errs errors.Error
178+
for _, x := range p.path {
179+
if err, ok := x.sel.(pathError); ok {
180+
errs = errors.Append(errs, err.Error)
181+
}
182+
}
183+
return errs
184+
}
185+
186+
func isHiddenOrDefinition(s string) bool {
187+
return strings.HasPrefix(s, "#") || strings.HasPrefix(s, "_")
188+
}
189+
190+
// A Def marks a string as a definition label. An # will be added if a string is
191+
// not prefixed with an # or _ already. Hidden labels are qualified by the
192+
// package in which they are looked up.
193+
func Def(s string) Selector {
194+
if !isHiddenOrDefinition(s) {
195+
s = "#" + s
196+
}
197+
return Selector{definitionSelector(s)}
198+
}
199+
200+
type definitionSelector string
201+
202+
// String returns the CUE representation of the definition.
203+
func (d definitionSelector) String() string {
204+
return string(d)
205+
}
206+
207+
func (d definitionSelector) kind() adt.FeatureType {
208+
switch {
209+
case strings.HasPrefix(string(d), "#"):
210+
return adt.DefinitionLabel
211+
case strings.HasPrefix(string(d), "_#"):
212+
return adt.HiddenDefinitionLabel
213+
case strings.HasPrefix(string(d), "_"):
214+
return adt.HiddenLabel
215+
default:
216+
return adt.StringLabel
217+
}
218+
}
219+
220+
func (d definitionSelector) feature(r adt.Runtime) adt.Feature {
221+
return adt.MakeIdentLabel(r, string(d), "")
222+
}
223+
224+
// A Str is a CUE string label. Definition selectors are defined with Def.
225+
func Str(s string) Selector {
226+
return Selector{stringSelector(s)}
227+
}
228+
229+
type stringSelector string
230+
231+
func (s stringSelector) String() string {
232+
str := string(s)
233+
if isHiddenOrDefinition(str) || !ast.IsValidIdent(str) {
234+
return literal.Label.Quote(str)
235+
}
236+
return str
237+
}
238+
239+
func (s stringSelector) kind() adt.FeatureType { return adt.StringLabel }
240+
241+
func (s stringSelector) feature(r adt.Runtime) adt.Feature {
242+
return adt.MakeStringLabel(r, string(s))
243+
}
244+
245+
// An Index selects a list element by index.
246+
func Index(x int) Selector {
247+
f, err := adt.MakeLabel(nil, int64(x), adt.IntLabel)
248+
if err != nil {
249+
return Selector{pathError{err}}
250+
}
251+
return Selector{indexSelector(f)}
252+
}
253+
254+
type indexSelector adt.Feature
255+
256+
func (s indexSelector) String() string {
257+
return strconv.Itoa(adt.Feature(s).Index())
258+
}
259+
260+
func (s indexSelector) kind() adt.FeatureType { return adt.IntLabel }
261+
262+
func (s indexSelector) feature(r adt.Runtime) adt.Feature {
263+
return adt.Feature(s)
264+
}
265+
266+
// TODO: allow import paths to be represented?
267+
//
268+
// // ImportPath defines a lookup at the root of an instance. It must be the first
269+
// // element of a Path.
270+
// func ImportPath(s string) Selector {
271+
// return importSelector(s)
272+
// }
273+
274+
// type importSelector string
275+
276+
// func (s importSelector) String() string {
277+
// return literal.String.Quote(string(s))
278+
// }
279+
280+
// func (s importSelector) feature(r adt.Runtime) adt.Feature {
281+
// return adt.InvalidLabel
282+
// }
283+
284+
// TODO: allow looking up in parent scopes?
285+
286+
// // Parent returns a Selector for looking up in the parent of a current node.
287+
// // Parent selectors may only occur at the start of a Path.
288+
// func Parent() Selector {
289+
// return parentSelector{}
290+
// }
291+
292+
// type parentSelector struct{}
293+
294+
// func (p parentSelector) String() string { return "__up" }
295+
// func (p parentSelector) feature(r adt.Runtime) adt.Feature {
296+
// return adt.InvalidLabel
297+
// }
298+
299+
type pathError struct {
300+
errors.Error
301+
}
302+
303+
func (p pathError) String() string { return p.Error.Error() }
304+
func (p pathError) kind() adt.FeatureType { return 0 }
305+
func (p pathError) feature(r adt.Runtime) adt.Feature {
306+
return adt.InvalidLabel
307+
}

0 commit comments

Comments
 (0)