-
Notifications
You must be signed in to change notification settings - Fork 276
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 <cueckoo@gmail.com> Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
- Loading branch information
Showing
3 changed files
with
484 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,307 @@ | ||
// Copyright 2020 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 cue | ||
|
||
import ( | ||
"strconv" | ||
"strings" | ||
|
||
"cuelang.org/go/cue/ast" | ||
"cuelang.org/go/cue/errors" | ||
"cuelang.org/go/cue/literal" | ||
"cuelang.org/go/cue/parser" | ||
"cuelang.org/go/cue/token" | ||
"cuelang.org/go/internal" | ||
"cuelang.org/go/internal/core/adt" | ||
"github.com/cockroachdb/apd/v2" | ||
) | ||
|
||
// A Selector is path component in a path. | ||
type Selector struct { | ||
sel selector | ||
} | ||
|
||
// String reports the CUE representation of a selector. | ||
func (sel Selector) String() string { | ||
return sel.sel.String() | ||
} | ||
|
||
type selector interface { | ||
String() string | ||
|
||
feature(ctx adt.Runtime) adt.Feature | ||
kind() adt.FeatureType | ||
} | ||
|
||
// A Path is series of selectors to query a CUE value. | ||
type Path struct { | ||
path []Selector | ||
} | ||
|
||
// MakePath creates a Path from a sequence of selectors. | ||
func MakePath(selectors ...Selector) Path { | ||
return Path{path: selectors} | ||
} | ||
|
||
// ParsePath parses a CUE expression into a Path. Any error resulting from | ||
// this conversion can be obtained by calling Err on the result. | ||
func ParsePath(s string) Path { | ||
expr, err := parser.ParseExpr("", s) | ||
if err != nil { | ||
return MakePath(Selector{pathError{errors.Promote(err, "invalid path")}}) | ||
} | ||
|
||
return Path{path: toSelectors(expr)} | ||
} | ||
|
||
// String reports the CUE representation of p. | ||
func (p Path) String() string { | ||
if err := p.Err(); err != nil { | ||
return "_|_" | ||
} | ||
|
||
b := &strings.Builder{} | ||
for i, sel := range p.path { | ||
x := sel.sel | ||
// TODO: use '.' in all cases, once supported. | ||
switch { | ||
case x.kind() == adt.IntLabel: | ||
b.WriteByte('[') | ||
b.WriteString(x.String()) | ||
b.WriteByte(']') | ||
continue | ||
case i > 0: | ||
b.WriteByte('.') | ||
} | ||
|
||
b.WriteString(x.String()) | ||
} | ||
return b.String() | ||
} | ||
|
||
func toSelectors(expr ast.Expr) []Selector { | ||
switch x := expr.(type) { | ||
case *ast.Ident: | ||
return []Selector{identSelector(x)} | ||
|
||
case *ast.IndexExpr: | ||
a := toSelectors(x.X) | ||
var sel Selector | ||
if b, ok := x.Index.(*ast.BasicLit); !ok { | ||
sel = Selector{pathError{ | ||
errors.Newf(token.NoPos, "non-constant expression %s", | ||
internal.DebugStr(x.Index))}} | ||
} else { | ||
sel = basicLitSelector(b) | ||
} | ||
return append(a, sel) | ||
|
||
case *ast.SelectorExpr: | ||
a := toSelectors(x.X) | ||
return append(a, identSelector(x.Sel)) | ||
|
||
default: | ||
return []Selector{Selector{pathError{ | ||
errors.Newf(token.NoPos, "invalid label %s ", internal.DebugStr(x)), | ||
}}} | ||
} | ||
} | ||
|
||
func basicLitSelector(b *ast.BasicLit) Selector { | ||
switch b.Kind { | ||
case token.INT: | ||
var n literal.NumInfo | ||
if err := literal.ParseNum(b.Value, &n); err != nil { | ||
return Selector{pathError{ | ||
errors.Newf(token.NoPos, "invalid string index %s", b.Value), | ||
}} | ||
} | ||
var d apd.Decimal | ||
_ = n.Decimal(&d) | ||
i, err := d.Int64() | ||
if err != nil { | ||
return Selector{pathError{ | ||
errors.Newf(token.NoPos, "integer %s out of range", b.Value), | ||
}} | ||
} | ||
return Index(int(i)) | ||
|
||
case token.STRING: | ||
info, _, _, _ := literal.ParseQuotes(b.Value, b.Value) | ||
if !info.IsDouble() { | ||
return Selector{pathError{ | ||
errors.Newf(token.NoPos, "invalid string index %s", b.Value)}} | ||
} | ||
s, _ := literal.Unquote(b.Value) | ||
return Selector{stringSelector(s)} | ||
|
||
default: | ||
return Selector{pathError{ | ||
errors.Newf(token.NoPos, "invalid literal %s", b.Value), | ||
}} | ||
} | ||
} | ||
|
||
func identSelector(label ast.Label) Selector { | ||
switch x := label.(type) { | ||
case *ast.Ident: | ||
if isHiddenOrDefinition(x.Name) { | ||
return Selector{definitionSelector(x.Name)} | ||
} | ||
return Selector{stringSelector(x.Name)} | ||
|
||
case *ast.BasicLit: | ||
return basicLitSelector(x) | ||
|
||
default: | ||
return Selector{pathError{ | ||
errors.Newf(token.NoPos, "invalid label %s ", internal.DebugStr(x)), | ||
}} | ||
} | ||
} | ||
|
||
// Err reports errors that occurred when generating the path. | ||
func (p Path) Err() error { | ||
var errs errors.Error | ||
for _, x := range p.path { | ||
if err, ok := x.sel.(pathError); ok { | ||
errs = errors.Append(errs, err.Error) | ||
} | ||
} | ||
return errs | ||
} | ||
|
||
func isHiddenOrDefinition(s string) bool { | ||
return strings.HasPrefix(s, "#") || strings.HasPrefix(s, "_") | ||
} | ||
|
||
// A Def marks a string as a definition label. An # will be added if a string is | ||
// not prefixed with an # or _ already. Hidden labels are qualified by the | ||
// package in which they are looked up. | ||
func Def(s string) Selector { | ||
if !isHiddenOrDefinition(s) { | ||
s = "#" + s | ||
} | ||
return Selector{definitionSelector(s)} | ||
} | ||
|
||
type definitionSelector string | ||
|
||
// String returns the CUE representation of the definition. | ||
func (d definitionSelector) String() string { | ||
return string(d) | ||
} | ||
|
||
func (d definitionSelector) kind() adt.FeatureType { | ||
switch { | ||
case strings.HasPrefix(string(d), "#"): | ||
return adt.DefinitionLabel | ||
case strings.HasPrefix(string(d), "_#"): | ||
return adt.HiddenDefinitionLabel | ||
case strings.HasPrefix(string(d), "_"): | ||
return adt.HiddenLabel | ||
default: | ||
return adt.StringLabel | ||
} | ||
} | ||
|
||
func (d definitionSelector) feature(r adt.Runtime) adt.Feature { | ||
return adt.MakeIdentLabel(r, string(d), "") | ||
} | ||
|
||
// A Str is a CUE string label. Definition selectors are defined with Def. | ||
func Str(s string) Selector { | ||
return Selector{stringSelector(s)} | ||
} | ||
|
||
type stringSelector string | ||
|
||
func (s stringSelector) String() string { | ||
str := string(s) | ||
if isHiddenOrDefinition(str) || !ast.IsValidIdent(str) { | ||
return literal.Label.Quote(str) | ||
} | ||
return str | ||
} | ||
|
||
func (s stringSelector) kind() adt.FeatureType { return adt.StringLabel } | ||
|
||
func (s stringSelector) feature(r adt.Runtime) adt.Feature { | ||
return adt.MakeStringLabel(r, string(s)) | ||
} | ||
|
||
// An Index selects a list element by index. | ||
func Index(x int) Selector { | ||
f, err := adt.MakeLabel(nil, int64(x), adt.IntLabel) | ||
if err != nil { | ||
return Selector{pathError{err}} | ||
} | ||
return Selector{indexSelector(f)} | ||
} | ||
|
||
type indexSelector adt.Feature | ||
|
||
func (s indexSelector) String() string { | ||
return strconv.Itoa(adt.Feature(s).Index()) | ||
} | ||
|
||
func (s indexSelector) kind() adt.FeatureType { return adt.IntLabel } | ||
|
||
func (s indexSelector) feature(r adt.Runtime) adt.Feature { | ||
return adt.Feature(s) | ||
} | ||
|
||
// TODO: allow import paths to be represented? | ||
// | ||
// // ImportPath defines a lookup at the root of an instance. It must be the first | ||
// // element of a Path. | ||
// func ImportPath(s string) Selector { | ||
// return importSelector(s) | ||
// } | ||
|
||
// type importSelector string | ||
|
||
// func (s importSelector) String() string { | ||
// return literal.String.Quote(string(s)) | ||
// } | ||
|
||
// func (s importSelector) feature(r adt.Runtime) adt.Feature { | ||
// return adt.InvalidLabel | ||
// } | ||
|
||
// TODO: allow looking up in parent scopes? | ||
|
||
// // Parent returns a Selector for looking up in the parent of a current node. | ||
// // Parent selectors may only occur at the start of a Path. | ||
// func Parent() Selector { | ||
// return parentSelector{} | ||
// } | ||
|
||
// type parentSelector struct{} | ||
|
||
// func (p parentSelector) String() string { return "__up" } | ||
// func (p parentSelector) feature(r adt.Runtime) adt.Feature { | ||
// return adt.InvalidLabel | ||
// } | ||
|
||
type pathError struct { | ||
errors.Error | ||
} | ||
|
||
func (p pathError) String() string { return p.Error.Error() } | ||
func (p pathError) kind() adt.FeatureType { return 0 } | ||
func (p pathError) feature(r adt.Runtime) adt.Feature { | ||
return adt.InvalidLabel | ||
} |
Oops, something went wrong.