add hcl2shim.PathFromFlatmapKey

PathFromFlatmapKey is used to convert a flatmap key to a cty.Path, and
ensures it conforms to the type schema.

This is used when handling Diffs, where the ResourceAttrDiffs are
indexed by the flatmapped key values, and we need to convert those to
addresses to apply to a cty.Value.
This commit is contained in:
James Bardin 2018-07-26 22:26:18 -04:00 committed by Martin Atkins
parent 6c01444a8a
commit 44e993ec29
2 changed files with 375 additions and 0 deletions

208
config/hcl2shim/paths.go Normal file
View File

@ -0,0 +1,208 @@
package hcl2shim
import (
"fmt"
"strconv"
"strings"
"github.com/zclconf/go-cty/cty"
)
// PathFromFlatmapKey takes a key from a flatmap along with the cty.Type
// describing the structure, and returns the cty.Path that would be used to
// reference the nested value in the data structure.
func PathFromFlatmapKey(k string, ty cty.Type) (cty.Path, error) {
if k == "" {
return nil, nil
}
if !ty.IsObjectType() {
panic(fmt.Sprintf("PathFromFlatmapKey called on %#v", ty))
}
path, err := pathFromFlatmapKeyObject(k, ty.AttributeTypes())
if err != nil {
return path, fmt.Errorf("[%s] %s", k, err)
}
return path, nil
}
func pathSplit(p string) (string, string) {
parts := strings.SplitN(p, ".", 2)
head := parts[0]
rest := ""
if len(parts) > 1 {
rest = parts[1]
}
return head, rest
}
func pathFromFlatmapKeyObject(key string, atys map[string]cty.Type) (cty.Path, error) {
k, rest := pathSplit(key)
path := cty.Path{cty.GetAttrStep{Name: k}}
ty, ok := atys[k]
if !ok {
return path, fmt.Errorf("attribute %q not found", key)
}
if rest == "" {
return path, nil
}
p, err := pathFromFlatmapKeyValue(rest, ty)
if err != nil {
return path, err
}
return append(path, p...), nil
}
func pathFromFlatmapKeyValue(key string, ty cty.Type) (cty.Path, error) {
var path cty.Path
var err error
switch {
case ty.IsPrimitiveType():
err = fmt.Errorf("invalid step %q with type %#v", key, ty)
case ty.IsObjectType():
path, err = pathFromFlatmapKeyObject(key, ty.AttributeTypes())
case ty.IsTupleType():
path, err = pathFromFlatmapKeyTuple(key, ty.TupleElementTypes())
case ty.IsMapType():
path, err = pathFromFlatmapKeyMap(key, ty)
case ty.IsListType():
path, err = pathFromFlatmapKeyList(key, ty)
case ty.IsSetType():
path, err = pathFromFlatmapKeySet(key, ty)
default:
err = fmt.Errorf("unrecognized type: %s", ty.FriendlyName())
}
if err != nil {
return path, err
}
return path, nil
}
func pathFromFlatmapKeyTuple(key string, etys []cty.Type) (cty.Path, error) {
var path cty.Path
var err error
k, rest := pathSplit(key)
// we don't need to convert the index keys to paths
if k == "#" {
return path, nil
}
idx, err := strconv.Atoi(k)
if err != nil {
return path, err
}
path = cty.Path{cty.IndexStep{Key: cty.NumberIntVal(int64(idx))}}
if idx >= len(etys) {
return path, fmt.Errorf("index %s out of range in %#v", key, etys)
}
if rest == "" {
return path, nil
}
ty := etys[idx]
p, err := pathFromFlatmapKeyValue(rest, ty.ElementType())
if err != nil {
return path, err
}
return append(path, p...), nil
}
func pathFromFlatmapKeyMap(key string, ty cty.Type) (cty.Path, error) {
var path cty.Path
var err error
k, rest := pathSplit(key)
// we don't need to convert the index keys to paths
if k == "%" {
return path, nil
}
path = cty.Path{cty.IndexStep{Key: cty.StringVal(k)}}
if rest == "" {
return path, nil
}
p, err := pathFromFlatmapKeyValue(rest, ty.ElementType())
if err != nil {
return path, err
}
return append(path, p...), nil
}
func pathFromFlatmapKeyList(key string, ty cty.Type) (cty.Path, error) {
var path cty.Path
var err error
k, rest := pathSplit(key)
// we don't need to convert the index keys to paths
if key == "#" {
return path, nil
}
idx, err := strconv.Atoi(k)
if err != nil {
return path, err
}
path = cty.Path{cty.IndexStep{Key: cty.NumberIntVal(int64(idx))}}
if rest == "" {
return path, nil
}
p, err := pathFromFlatmapKeyValue(rest, ty.ElementType())
if err != nil {
return path, err
}
return append(path, p...), nil
}
func pathFromFlatmapKeySet(key string, ty cty.Type) (cty.Path, error) {
var path cty.Path
var err error
k, rest := pathSplit(key)
// we don't need to convert the index keys to paths
if k == "#" {
return path, nil
}
idx, err := strconv.Atoi(k)
if err != nil {
return path, err
}
path = cty.Path{cty.IndexStep{Key: cty.NumberIntVal(int64(idx))}}
if rest == "" {
return path, nil
}
p, err := pathFromFlatmapKeyValue(rest, ty.ElementType())
if err != nil {
return path, err
}
return append(path, p...), nil
}

View File

@ -0,0 +1,167 @@
package hcl2shim
import (
"fmt"
"strings"
"testing"
"github.com/go-test/deep"
"github.com/zclconf/go-cty/cty"
)
func TestPathFromFlatmap(t *testing.T) {
tests := []struct {
Flatmap string
Type cty.Type
Want cty.Path
WantErr string
}{
{
Flatmap: "",
Type: cty.EmptyObject,
Want: nil,
},
{
Flatmap: "attr",
Type: cty.EmptyObject,
Want: nil,
WantErr: `attribute "attr" not found`,
},
{
Flatmap: "foo",
Type: cty.Object(map[string]cty.Type{
"foo": cty.String,
}),
Want: cty.Path{
cty.GetAttrStep{Name: "foo"},
},
},
{
Flatmap: "foo.#",
Type: cty.Object(map[string]cty.Type{
"foo": cty.List(cty.String),
}),
Want: cty.Path{
cty.GetAttrStep{Name: "foo"},
},
},
{
Flatmap: "foo.1",
Type: cty.Object(map[string]cty.Type{
"foo": cty.List(cty.String),
}),
Want: cty.Path{
cty.GetAttrStep{Name: "foo"},
cty.IndexStep{Key: cty.NumberIntVal(1)},
},
},
{
Flatmap: "foo.1",
Type: cty.Object(map[string]cty.Type{
"foo": cty.Tuple([]cty.Type{
cty.String,
cty.Bool,
}),
}),
Want: cty.Path{
cty.GetAttrStep{Name: "foo"},
cty.IndexStep{Key: cty.NumberIntVal(1)},
},
},
{
Flatmap: "foo.24534534",
Type: cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.String),
}),
Want: cty.Path{
cty.GetAttrStep{Name: "foo"},
cty.IndexStep{Key: cty.NumberIntVal(24534534)},
},
},
{
Flatmap: "foo.%",
Type: cty.Object(map[string]cty.Type{
"foo": cty.Map(cty.String),
}),
Want: cty.Path{
cty.GetAttrStep{Name: "foo"},
},
},
{
Flatmap: "foo.baz",
//FlatMap: "foo.bar.baz",
Type: cty.Object(map[string]cty.Type{
"foo": cty.Map(cty.Bool),
}),
Want: cty.Path{
cty.GetAttrStep{Name: "foo"},
cty.IndexStep{Key: cty.StringVal("baz")},
},
},
{
Flatmap: "foo.bar.baz",
Type: cty.Object(map[string]cty.Type{
"foo": cty.Map(
cty.Map(cty.Bool),
),
}),
Want: cty.Path{
cty.GetAttrStep{Name: "foo"},
cty.IndexStep{Key: cty.StringVal("bar")},
cty.IndexStep{Key: cty.StringVal("baz")},
},
},
{
Flatmap: "foo.0.bar",
Type: cty.Object(map[string]cty.Type{
"foo": cty.List(cty.Object(map[string]cty.Type{
"bar": cty.String,
"baz": cty.Bool,
})),
}),
Want: cty.Path{
cty.GetAttrStep{Name: "foo"},
cty.IndexStep{Key: cty.NumberIntVal(0)},
cty.GetAttrStep{Name: "bar"},
},
},
{
Flatmap: "foo.34534534.baz",
Type: cty.Object(map[string]cty.Type{
"foo": cty.Set(cty.Object(map[string]cty.Type{
"bar": cty.String,
"baz": cty.Bool,
})),
}),
Want: cty.Path{
cty.GetAttrStep{Name: "foo"},
cty.IndexStep{Key: cty.NumberIntVal(34534534)},
cty.GetAttrStep{Name: "baz"},
},
},
}
for _, test := range tests {
t.Run(fmt.Sprintf("%s as %#v", test.Flatmap, test.Type), func(t *testing.T) {
got, err := PathFromFlatmapKey(test.Flatmap, test.Type)
if test.WantErr != "" {
if err == nil {
t.Fatalf("succeeded; want error: %s", test.WantErr)
}
if got, want := err.Error(), test.WantErr; !strings.Contains(got, want) {
t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want)
}
return
} else {
if err != nil {
t.Fatalf("unexpected error: %s", err.Error())
}
}
for _, problem := range deep.Equal(got, test.Want) {
t.Error(problem)
}
})
}
}