hcl: AbsTraversalForExpr and RelTraversalForExpr
These functions permit a calling application to recognize when an expression represents a static absolute traversal and obtain that traversal. This allows for the unusual-but-valid case where an application wishes to access the expression source rather than its resulting value, when the expression source is something that can be understood as a traversal. An example use-case is an attribute that takes a list of other attributes it depends on, expressed as traversals. In this case the calling application needs to access the attribute names themselves rather than their values, e.g. to build some sort of dependency graph to gradually populate the scope for evaluation.
This commit is contained in:
parent
44bad6dbf5
commit
0949d55133
@ -70,6 +70,11 @@ func (e *ScopeTraversalExpr) StartRange() hcl.Range {
|
||||
return e.SrcRange
|
||||
}
|
||||
|
||||
// Implementation for hcl.AbsTraversalForExpr.
|
||||
func (e *ScopeTraversalExpr) AsTraversal() hcl.Traversal {
|
||||
return e.Traversal
|
||||
}
|
||||
|
||||
// RelativeTraversalExpr is an Expression that retrieves a value from another
|
||||
// value using a _relative_ traversal.
|
||||
type RelativeTraversalExpr struct {
|
||||
|
@ -1087,3 +1087,17 @@ func TestFunctionCallExprValue(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpressionAsTraversal(t *testing.T) {
|
||||
expr, _ := ParseExpression([]byte("a.b[0]"), "", hcl.Pos{})
|
||||
traversal, diags := hcl.AbsTraversalForExpr(expr)
|
||||
if len(diags) != 0 {
|
||||
t.Fatalf("unexpected diagnostics")
|
||||
}
|
||||
if len(traversal) != 3 {
|
||||
t.Fatalf("wrong traversal %#v; want length 3", traversal)
|
||||
}
|
||||
if traversal.RootName() != "a" {
|
||||
t.Fatalf("wrong root name %q; want %q", traversal.RootName(), "a")
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,8 @@ package json
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
"github.com/hashicorp/hcl2/hcl"
|
||||
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
)
|
||||
|
||||
@ -353,3 +353,20 @@ func (e *expression) Range() hcl.Range {
|
||||
func (e *expression) StartRange() hcl.Range {
|
||||
return e.src.StartRange()
|
||||
}
|
||||
|
||||
// Implementation for hcl.AbsTraversalForExpr.
|
||||
func (e *expression) AsTraversal() hcl.Traversal {
|
||||
// In JSON-based syntax a traversal is given as a string containing
|
||||
// traversal syntax as defined by hclsyntax.ParseTraversalAbs.
|
||||
|
||||
switch v := e.src.(type) {
|
||||
case *stringVal:
|
||||
traversal, diags := hclsyntax.ParseTraversalAbs([]byte(v.Value), v.SrcRange.Filename, v.SrcRange.Start)
|
||||
if diags.HasErrors() {
|
||||
return nil
|
||||
}
|
||||
return traversal
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
@ -741,3 +741,15 @@ func TestJustAttributes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpressionAsTraversal(t *testing.T) {
|
||||
e := &expression{
|
||||
src: &stringVal{
|
||||
Value: "foo.bar[0]",
|
||||
},
|
||||
}
|
||||
traversal := e.AsTraversal()
|
||||
if len(traversal) != 3 {
|
||||
t.Fatalf("incorrect traversal %#v; want length 3", traversal)
|
||||
}
|
||||
}
|
||||
|
55
hcl/traversal_for_expr.go
Normal file
55
hcl/traversal_for_expr.go
Normal file
@ -0,0 +1,55 @@
|
||||
package hcl
|
||||
|
||||
// AbsTraversalForExpr attempts to interpret the given expression as
|
||||
// an absolute traversal, or returns error diagnostic(s) if that is
|
||||
// not possible for the given expression.
|
||||
//
|
||||
// A particular Expression implementation can support this function by
|
||||
// offering a method called AsTraversal that takes no arguments and
|
||||
// returns either a valid absolute traversal or nil to indicate that
|
||||
// no traversal is possible.
|
||||
//
|
||||
// In most cases the calling application is interested in the value
|
||||
// that results from an expression, but in rarer cases the application
|
||||
// needs to see the the name of the variable and subsequent
|
||||
// attributes/indexes itself, for example to allow users to give references
|
||||
// to the variables themselves rather than to their values. An implementer
|
||||
// of this function should at least support attribute and index steps.
|
||||
func AbsTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
|
||||
type asTraversal interface {
|
||||
AsTraversal() Traversal
|
||||
}
|
||||
|
||||
if asT, supported := expr.(asTraversal); supported {
|
||||
if traversal := asT.AsTraversal(); traversal != nil {
|
||||
return traversal, nil
|
||||
}
|
||||
}
|
||||
return nil, Diagnostics{
|
||||
&Diagnostic{
|
||||
Severity: DiagError,
|
||||
Summary: "Invalid expression",
|
||||
Detail: "A static variable reference is required.",
|
||||
Subject: expr.Range().Ptr(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RelTraversalForExpr is similar to AbsTraversalForExpr but it returns
|
||||
// a relative traversal instead. Due to the nature of ZCL expressions, the
|
||||
// first element of the returned traversal is always a TraverseAttr, and
|
||||
// then it will be followed by zero or more other expressions.
|
||||
//
|
||||
// Any expression accepted by AbsTraversalForExpr is also accepted by
|
||||
// RelTraversalForExpr.
|
||||
func RelTraversalForExpr(expr Expression) (Traversal, Diagnostics) {
|
||||
traversal, diags := AbsTraversalForExpr(expr)
|
||||
if len(traversal) > 0 {
|
||||
root := traversal[0].(TraverseRoot)
|
||||
traversal[0] = TraverseAttr{
|
||||
Name: root.Name,
|
||||
SrcRange: root.SrcRange,
|
||||
}
|
||||
}
|
||||
return traversal, diags
|
||||
}
|
128
hcl/traversal_for_expr_test.go
Normal file
128
hcl/traversal_for_expr_test.go
Normal file
@ -0,0 +1,128 @@
|
||||
package hcl
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type asTraversalSupported struct {
|
||||
staticExpr
|
||||
RootName string
|
||||
}
|
||||
|
||||
type asTraversalNotSupported struct {
|
||||
staticExpr
|
||||
}
|
||||
|
||||
type asTraversalDeclined struct {
|
||||
staticExpr
|
||||
}
|
||||
|
||||
func (e asTraversalSupported) AsTraversal() Traversal {
|
||||
return Traversal{
|
||||
TraverseRoot{
|
||||
Name: e.RootName,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e asTraversalDeclined) AsTraversal() Traversal {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAbsTraversalForExpr(t *testing.T) {
|
||||
tests := []struct {
|
||||
Expr Expression
|
||||
WantRootName string
|
||||
}{
|
||||
{
|
||||
asTraversalSupported{RootName: "foo"},
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
asTraversalNotSupported{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
asTraversalDeclined{},
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
got, diags := AbsTraversalForExpr(test.Expr)
|
||||
switch {
|
||||
case got != nil:
|
||||
if test.WantRootName == "" {
|
||||
t.Fatalf("traversal was returned; want error")
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("wrong traversal length %d; want 1", len(got))
|
||||
}
|
||||
gotRoot, ok := got[0].(TraverseRoot)
|
||||
if !ok {
|
||||
t.Fatalf("first traversal step is %T; want hcl.TraverseRoot", got[0])
|
||||
}
|
||||
if gotRoot.Name != test.WantRootName {
|
||||
t.Errorf("wrong root name %q; want %q", gotRoot.Name, test.WantRootName)
|
||||
}
|
||||
default:
|
||||
if !diags.HasErrors() {
|
||||
t.Errorf("returned nil traversal without error diagnostics")
|
||||
}
|
||||
if test.WantRootName != "" {
|
||||
t.Errorf("traversal was not returned; want TraverseRoot(%q)", test.WantRootName)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelTraversalForExpr(t *testing.T) {
|
||||
tests := []struct {
|
||||
Expr Expression
|
||||
WantFirstName string
|
||||
}{
|
||||
{
|
||||
asTraversalSupported{RootName: "foo"},
|
||||
"foo",
|
||||
},
|
||||
{
|
||||
asTraversalNotSupported{},
|
||||
"",
|
||||
},
|
||||
{
|
||||
asTraversalDeclined{},
|
||||
"",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run("", func(t *testing.T) {
|
||||
got, diags := RelTraversalForExpr(test.Expr)
|
||||
switch {
|
||||
case got != nil:
|
||||
if test.WantFirstName == "" {
|
||||
t.Fatalf("traversal was returned; want error")
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("wrong traversal length %d; want 1", len(got))
|
||||
}
|
||||
gotRoot, ok := got[0].(TraverseAttr)
|
||||
if !ok {
|
||||
t.Fatalf("first traversal step is %T; want hcl.TraverseAttr", got[0])
|
||||
}
|
||||
if gotRoot.Name != test.WantFirstName {
|
||||
t.Errorf("wrong root name %q; want %q", gotRoot.Name, test.WantFirstName)
|
||||
}
|
||||
default:
|
||||
if !diags.HasErrors() {
|
||||
t.Errorf("returned nil traversal without error diagnostics")
|
||||
}
|
||||
if test.WantFirstName != "" {
|
||||
t.Errorf("traversal was not returned; want TraverseAttr(%q)", test.WantFirstName)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user