diff --git a/cue/types.go b/cue/types.go index 1af4a2a31..fb8961f79 100644 --- a/cue/types.go +++ b/cue/types.go @@ -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 { diff --git a/cue/types_test.go b/cue/types_test.go index 088e171a2..a327ea85a 100644 --- a/cue/types_test.go +++ b/cue/types_test.go @@ -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: { @@ -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 { diff --git a/internal/attrs.go b/internal/attrs.go index 4eeeb6ee0..058948012 100644 --- a/internal/attrs.go +++ b/internal/attrs.go @@ -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 } @@ -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:]) } @@ -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.