hcl: New GetAttr and ApplyPath functions

These make it easier for calling applications to get the same result as
operators within HCL expressions both for individual attribute accesses
and when processing whole cty.Paths.

(We previously had an Index function which did the same thing for
indexing, and ApplyPath is just a wrapper around calling GetAttr and Index
in a loop.)
This commit is contained in:
Martin Atkins 2019-01-30 14:52:18 -08:00
parent 93fb31f28b
commit 89dbc5eb3d
3 changed files with 252 additions and 60 deletions

View File

@ -145,3 +145,122 @@ func Index(collection, key cty.Value, srcRange *Range) (cty.Value, Diagnostics)
}
}
// GetAttr is a helper function that performs the same operation as the
// attribute access in the HCL expression language. That is, the result is the
// same as it would be for obj.attr in a configuration expression.
//
// This is exported so that applications can access attributes in a manner
// consistent with how the language does it, including handling of null and
// unknown values, etc.
//
// Diagnostics are produced if the given combination of values is not valid.
// Therefore a pointer to a source range must be provided to use in diagnostics,
// though nil can be provided if the calling application is going to
// ignore the subject of the returned diagnostics anyway.
func GetAttr(obj cty.Value, attrName string, srcRange *Range) (cty.Value, Diagnostics) {
if obj.IsNull() {
return cty.DynamicVal, Diagnostics{
{
Severity: DiagError,
Summary: "Attempt to get attribute from null value",
Detail: "This value is null, so it does not have any attributes.",
Subject: srcRange,
},
}
}
ty := obj.Type()
switch {
case ty.IsObjectType():
if !ty.HasAttribute(attrName) {
return cty.DynamicVal, Diagnostics{
{
Severity: DiagError,
Summary: "Unsupported attribute",
Detail: fmt.Sprintf("This object does not have an attribute named %q.", attrName),
Subject: srcRange,
},
}
}
if !obj.IsKnown() {
return cty.UnknownVal(ty.AttributeType(attrName)), nil
}
return obj.GetAttr(attrName), nil
case ty.IsMapType():
if !obj.IsKnown() {
return cty.UnknownVal(ty.ElementType()), nil
}
idx := cty.StringVal(attrName)
if obj.HasIndex(idx).False() {
return cty.DynamicVal, Diagnostics{
{
Severity: DiagError,
Summary: "Missing map element",
Detail: fmt.Sprintf("This map does not have an element with the key %q.", attrName),
Subject: srcRange,
},
}
}
return obj.Index(idx), nil
case ty == cty.DynamicPseudoType:
return cty.DynamicVal, nil
default:
return cty.DynamicVal, Diagnostics{
{
Severity: DiagError,
Summary: "Unsupported attribute",
Detail: "This value does not have any attributes.",
Subject: srcRange,
},
}
}
}
// ApplyPath is a helper function that applies a cty.Path to a value using the
// indexing and attribute access operations from HCL.
//
// This is similar to calling the path's own Apply method, but ApplyPath uses
// the more relaxed typing rules that apply to these operations in HCL, rather
// than cty's relatively-strict rules. ApplyPath is implemented in terms of
// Index and GetAttr, and so it has the same behavior for individual steps
// but will stop and return any errors returned by intermediate steps.
//
// Diagnostics are produced if the given path cannot be applied to the given
// value. Therefore a pointer to a source range must be provided to use in
// diagnostics, though nil can be provided if the calling application is going
// to ignore the subject of the returned diagnostics anyway.
func ApplyPath(val cty.Value, path cty.Path, srcRange *Range) (cty.Value, Diagnostics) {
var diags Diagnostics
for _, step := range path {
var stepDiags Diagnostics
switch ts := step.(type) {
case cty.IndexStep:
val, stepDiags = Index(val, ts.Key, srcRange)
case cty.GetAttrStep:
val, stepDiags = GetAttr(val, ts.Name, srcRange)
default:
// Should never happen because the above are all of the step types.
diags = diags.Append(&Diagnostic{
Severity: DiagError,
Summary: "Invalid path step",
Detail: fmt.Sprintf("Go type %T is not a valid path step. This is a bug in this program.", step),
Subject: srcRange,
})
return cty.DynamicVal, diags
}
diags = append(diags, stepDiags...)
if stepDiags.HasErrors() {
return cty.DynamicVal, diags
}
}
return val, diags
}

132
hcl/ops_test.go Normal file
View File

@ -0,0 +1,132 @@
package hcl
import (
"fmt"
"testing"
"github.com/zclconf/go-cty/cty"
)
func TestApplyPath(t *testing.T) {
tests := []struct {
Start cty.Value
Path cty.Path
Want cty.Value
WantErr string
}{
{
cty.StringVal("hello"),
nil,
cty.StringVal("hello"),
``,
},
{
cty.StringVal("hello"),
(cty.Path)(nil).Index(cty.StringVal("boop")),
cty.NilVal,
`Invalid index`,
},
{
cty.StringVal("hello"),
(cty.Path)(nil).Index(cty.NumberIntVal(0)),
cty.NilVal,
`Invalid index`,
},
{
cty.ListVal([]cty.Value{
cty.StringVal("hello"),
}),
(cty.Path)(nil).Index(cty.NumberIntVal(0)),
cty.StringVal("hello"),
``,
},
{
cty.TupleVal([]cty.Value{
cty.StringVal("hello"),
}),
(cty.Path)(nil).Index(cty.NumberIntVal(0)),
cty.StringVal("hello"),
``,
},
{
cty.ListValEmpty(cty.String),
(cty.Path)(nil).Index(cty.NumberIntVal(0)),
cty.NilVal,
`Invalid index`,
},
{
cty.ListVal([]cty.Value{
cty.StringVal("hello"),
}),
(cty.Path)(nil).Index(cty.NumberIntVal(1)),
cty.NilVal,
`Invalid index`,
},
{
cty.ListVal([]cty.Value{
cty.StringVal("hello"),
}),
(cty.Path)(nil).Index(cty.NumberIntVal(0)).GetAttr("foo"),
cty.NilVal,
`Unsupported attribute`,
},
{
cty.ListVal([]cty.Value{
cty.EmptyObjectVal,
}),
(cty.Path)(nil).Index(cty.NumberIntVal(0)).GetAttr("foo"),
cty.NilVal,
`Unsupported attribute`,
},
{
cty.NullVal(cty.List(cty.String)),
(cty.Path)(nil).Index(cty.NumberIntVal(0)),
cty.NilVal,
`Attempt to index null value`,
},
{
cty.NullVal(cty.Map(cty.String)),
(cty.Path)(nil).Index(cty.NumberIntVal(0)),
cty.NilVal,
`Attempt to index null value`,
},
{
cty.NullVal(cty.EmptyObject),
(cty.Path)(nil).GetAttr("foo"),
cty.NilVal,
`Attempt to get attribute from null value`,
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%#v %#v", test.Start, test.Path), func(t *testing.T) {
got, diags := ApplyPath(test.Start, test.Path, nil)
t.Logf("testing ApplyPath\nstart: %#v\npath: %#v", test.Start, test.Path)
for _, diag := range diags {
t.Logf(diag.Error())
}
if test.WantErr != "" {
if !diags.HasErrors() {
t.Fatalf("succeeded, but want error\nwant error: %s", test.WantErr)
}
if len(diags) != 1 {
t.Fatalf("wrong number of diagnostics %d; want 1", len(diags))
}
if gotErrStr := diags[0].Summary; gotErrStr != test.WantErr {
t.Fatalf("wrong error\ngot error: %s\nwant error: %s", gotErrStr, test.WantErr)
}
return
}
if diags.HasErrors() {
t.Fatalf("failed, but want success\ngot diagnostics:\n%s", diags.Error())
}
if !test.Want.RawEquals(got) {
t.Fatalf("wrong result\ngot: %#v\nwant: %#v", got, test.Want)
}
})
}
}

View File

@ -255,66 +255,7 @@ type TraverseAttr struct {
}
func (tn TraverseAttr) TraversalStep(val cty.Value) (cty.Value, Diagnostics) {
if val.IsNull() {
return cty.DynamicVal, Diagnostics{
{
Severity: DiagError,
Summary: "Attempt to get attribute from null value",
Detail: "This value is null, so it does not have any attributes.",
Subject: &tn.SrcRange,
},
}
}
ty := val.Type()
switch {
case ty.IsObjectType():
if !ty.HasAttribute(tn.Name) {
return cty.DynamicVal, Diagnostics{
{
Severity: DiagError,
Summary: "Unsupported attribute",
Detail: fmt.Sprintf("This object does not have an attribute named %q.", tn.Name),
Subject: &tn.SrcRange,
},
}
}
if !val.IsKnown() {
return cty.UnknownVal(ty.AttributeType(tn.Name)), nil
}
return val.GetAttr(tn.Name), nil
case ty.IsMapType():
if !val.IsKnown() {
return cty.UnknownVal(ty.ElementType()), nil
}
idx := cty.StringVal(tn.Name)
if val.HasIndex(idx).False() {
return cty.DynamicVal, Diagnostics{
{
Severity: DiagError,
Summary: "Missing map element",
Detail: fmt.Sprintf("This map does not have an element with the key %q.", tn.Name),
Subject: &tn.SrcRange,
},
}
}
return val.Index(idx), nil
case ty == cty.DynamicPseudoType:
return cty.DynamicVal, nil
default:
return cty.DynamicVal, Diagnostics{
{
Severity: DiagError,
Summary: "Unsupported attribute",
Detail: "This value does not have any attributes.",
Subject: &tn.SrcRange,
},
}
}
return GetAttr(val, tn.Name, &tn.SrcRange)
}
func (tn TraverseAttr) SourceRange() Range {