Skip to content
This repository has been archived by the owner on Nov 18, 2021. It is now read-only.

Commit

Permalink
cue: add function for Value.Attributes()
Browse files Browse the repository at this point in the history
First attempt by @verdverm, significant edits by @mpvl.

The API has been designed so that new attribute types can
be added without breaking anything: the user will always
have to explicitly specify the types of attributes that
are needed.

This also adds the NumArgs, Arg, and RawArg methods
to give access to the attribute fields.

This does not handle duplicate attributes. In the future,
there could be merge functionality for this purpose. This
could be done by adding option flags if we can settle on
what constitutes good options.

Original comments from Tony:

This enables a user of the Go API to get all attributes so they
can inspect them, rather than only being able to ask if a particular
attribute exists on a value. Really useful for tools leveraging CUE!

I recall @mpvl might have wanted the return to be a Value or List
rather than the slice?

Closes #529
#529

GitOrigin-RevId: 6a8951a
Change-Id: Ie3cc9983656864fa44852b924a3142c82e336f4a
Reviewed-on: https://cue-review.googlesource.com/c/cue/+/9201
Reviewed-by: CUE cueckoo <cueckoo@gmail.com>
Reviewed-by: Marcel van Lohuizen <mpvl@golang.org>
  • Loading branch information
verdverm authored and mpvl committed Mar 31, 2021
1 parent b1730b6 commit 792da39
Show file tree
Hide file tree
Showing 3 changed files with 204 additions and 11 deletions.
118 changes: 114 additions & 4 deletions cue/types.go
Expand Up @@ -2163,25 +2163,135 @@ func (v Value) Walk(before func(Value) bool, after func(Value)) {
func (v Value) Attribute(key string) Attribute {
// look up the attributes
if v.v == nil {
return Attribute{internal.NewNonExisting(key)}
return nonExistAttr(key)
}
// look up the attributes
for _, a := range export.ExtractFieldAttrs(v.v.Conjuncts) {
k, body := a.Split()
k, _ := a.Split()
if key != k {
continue
}
return Attribute{internal.ParseAttrBody(token.NoPos, body)}
return newAttr(internal.FieldAttr, a)
}

return Attribute{internal.NewNonExisting(key)}
return nonExistAttr(key)
}

func newAttr(k internal.AttrKind, a *ast.Attribute) Attribute {
key, body := a.Split()
x := internal.ParseAttrBody(token.NoPos, body)
x.Name = key
x.Kind = k
return Attribute{x}
}

func nonExistAttr(key string) Attribute {
a := internal.NewNonExisting(key)
a.Name = key
a.Kind = internal.FieldAttr
return Attribute{a}
}

// Attributes reports all field attributes for the Value.
//
// To retrieve attributes of multiple kinds, you can bitwise-or kinds together.
// Use ValueKind to query attributes associated with a value.
func (v Value) Attributes(mask AttrKind) []Attribute {
if v.v == nil {
return nil
}

attrs := []Attribute{}

if mask&FieldAttr != 0 {
for _, a := range export.ExtractFieldAttrs(v.v.Conjuncts) {
attrs = append(attrs, newAttr(internal.FieldAttr, a))
}
}

if mask&DeclAttr != 0 {
for _, a := range export.ExtractDeclAttrs(v.v.Conjuncts) {
attrs = append(attrs, newAttr(internal.DeclAttr, a))
}
}

return attrs
}

// AttrKind indicates the location of an attribute within CUE source.
type AttrKind int

const (
// FieldAttr indicates a field attribute.
// foo: bar @attr()
FieldAttr AttrKind = AttrKind(internal.FieldAttr)

// DeclAttr indicates a declaration attribute.
// foo: {
// @attr()
// }
DeclAttr AttrKind = AttrKind(internal.DeclAttr)

// A ValueAttr is a bit mask to request any attribute that is locally
// associated with a field, instead of, for instance, an entire file.
ValueAttr AttrKind = FieldAttr | DeclAttr

// TODO: Possible future attr kinds
// ElemAttr (is a ValueAttr)
// FileAttr (not a ValueAttr)

// TODO: Merge: merge namesake attributes.
)

// An Attribute contains meta data about a field.
type Attribute struct {
attr internal.Attr
}

// Format implements fmt.Formatter.
func (a Attribute) Format(w fmt.State, verb rune) {
fmt.Fprintf(w, "@%s(%s)", a.attr.Name, a.attr.Body)
}

var _ fmt.Formatter = &Attribute{}

// Name returns the name of the attribute, for instance, "json" for @json(...).
func (a *Attribute) Name() string {
return a.attr.Name
}

// Contents reports the full contents of an attribute within parentheses, so
// contents in @attr(contents).
func (a *Attribute) Contents() string {
return a.attr.Body
}

// NumArgs reports the number of arguments parsed for this attribute.
func (a *Attribute) NumArgs() int {
return len(a.attr.Fields)
}

// Arg reports the contents of the ith comma-separated argument of a.
//
// If the argument contains an unescaped equals sign, it returns a key-value
// pair. Otherwise it returns the contents in value.
func (a *Attribute) Arg(i int) (key, value string) {
f := a.attr.Fields[i]
return f.Key(), f.Value()
}

// RawArg reports the raw contents of the ith comma-separated argument of a,
// including surrounding spaces.
func (a *Attribute) RawArg(i int) string {
return a.attr.Fields[i].Text()
}

// Kind reports the type of location within CUE source where the attribute
// was specified.
func (a *Attribute) Kind() AttrKind {
return AttrKind(a.attr.Kind)
}

// Err returns the error associated with this Attribute or nil if this
// attribute is valid.
func (a *Attribute) Err() error {
Expand Down
53 changes: 52 additions & 1 deletion cue/types_test.go
Expand Up @@ -1994,6 +1994,57 @@ func cmpError(a, b error) bool {
return a.Error() == b.Error()
}

func TestAttributes(t *testing.T) {
const config = `
a: {
a: 0 @foo(a,b,c=1)
b: 1 @bar(a,b,c,d=1) @foo(a,,d=1)
}
b: {
@embed(foo)
3
} @field(foo)
`

testCases := []struct {
flags AttrKind
path string
out string
}{{
flags: FieldAttr,
path: "a.a",
out: "[@foo(a,b,c=1)]",
}, {
flags: FieldAttr,
path: "a.b",
out: "[@bar(a,b,c,d=1) @foo(a,,d=1)]",
}, {
flags: DeclAttr,
path: "b",
out: "[@embed(foo)]",
}, {
flags: FieldAttr,
path: "b",
out: "[@field(foo)]",
}, {
flags: ValueAttr,
path: "b",
out: "[@field(foo) @embed(foo)]",
}}
for _, tc := range testCases {
t.Run("", func(t *testing.T) {
v := getInstance(t, config).Value().LookupPath(ParsePath(tc.path))
a := v.Attributes(tc.flags)
got := fmt.Sprint(a)
if got != tc.out {
t.Errorf("got %v; want %v", got, tc.out)
}

})
}
}

func TestAttributeErr(t *testing.T) {
const config = `
a: {
Expand Down Expand Up @@ -2201,7 +2252,7 @@ func TestAttributeLookup(t *testing.T) {
const config = `
a: {
a: 0 @foo(a,b,c=1)
b: 1 @bar(a,b,e=-5,d=1) @foo(a,,d=1)
b: 1 @bar(a,b,e =-5,d=1) @foo(a,,d=1)
}
`
testCases := []struct {
Expand Down
44 changes: 38 additions & 6 deletions internal/attrs.go
Expand Up @@ -25,10 +25,32 @@ import (
"cuelang.org/go/cue/token"
)

// AttrKind indicates the location of an attribute within CUE source.
type AttrKind uint8

const (
// FieldAttr indicates an attribute is a field attribute.
// foo: bar @attr()
FieldAttr AttrKind = 1 << iota

// DeclAttr indicates an attribute was specified at a declaration position.
// foo: {
// @attr()
// }
DeclAttr

// TODO: Possible future attr kinds
// ElemAttr
// FileAttr
// ValueAttr = FieldAttr|DeclAttr|ElemAttr
)

// Attr holds positional information for a single Attr.
type Attr struct {
Name string // e.g. "json" or "protobuf"
Body string
Fields []keyValue
Kind AttrKind
Fields []KeyValue
Err error
}

Expand All @@ -38,14 +60,24 @@ func NewNonExisting(key string) Attr {
return Attr{Err: errors.Newf(token.NoPos, msgNotExist, key)}
}

type keyValue struct {
type KeyValue struct {
data string
equal int // index of equal sign or 0 if non-existing
}

func (kv *keyValue) Text() string { return kv.data }
func (kv *keyValue) Key() string { return kv.data[:kv.equal] }
func (kv *keyValue) Value() string {
func (kv *KeyValue) Text() string { return kv.data }
func (kv *KeyValue) Key() string {
if kv.equal == 0 {
return kv.data
}
s := kv.data[:kv.equal]
s = strings.TrimSpace(s)
return s
}
func (kv *KeyValue) Value() string {
if kv.equal == 0 {
return ""
}
return strings.TrimSpace(kv.data[kv.equal+1:])
}

Expand Down Expand Up @@ -143,7 +175,7 @@ func skipSpace(s string) int {

func scanAttributeElem(pos token.Pos, s string, a *Attr) (n int, err errors.Error) {
// try CUE string
kv := keyValue{}
kv := KeyValue{}
if n, kv.data, err = scanAttributeString(pos, s); n == 0 {
// try key-value pair
p := strings.IndexAny(s, ",=") // ) is assumed to be stripped.
Expand Down

0 comments on commit 792da39

Please sign in to comment.