diff --git a/cty/path.go b/cty/path.go index bf1a7c15..dcb92161 100644 --- a/cty/path.go +++ b/cty/path.go @@ -71,6 +71,77 @@ func (p Path) GetAttr(name string) Path { return ret } +// Matches compare the paths for equality. +// An IndexStep with an Unknown Key value acts as a wildcard, matching any +// value of the same type. +func (p Path) Matches(other Path) bool { + max := len(p) + if len(other) > max { + max = len(other) + } + + // 2 empty paths are equal + if max == 0 { + return true + } + + for i := 0; i < max; i++ { + if i >= len(p) || i >= len(other) { + // At this point, the paths are only equal if they have the same + // length. Any wildcards would have returned earlier from the + // unknown checks. + return false + } + + pv := p[i] + switch pv := pv.(type) { + case GetAttrStep: + ov, ok := other[i].(GetAttrStep) + if !ok || pv != ov { + return false + } + case IndexStep: + ov, ok := other[i].(IndexStep) + if !ok { + return false + } + + if !pv.Key.Type().Equals(ov.Key.Type()) { + return false + } + + // If a path element is unknown, it can match any element of the + // same type. + if !pv.Key.IsKnown() || !ov.Key.IsKnown() { + // If that final value in a path is unknown, we can allow a + // shorter path to match a longer one. + // This is the only case wehere we return true early. + switch { + case !pv.Key.IsKnown() && i == len(p)-1: + return true + case !ov.Key.IsKnown() && i == len(other)-1: + return true + } + + continue + } + + eq := pv.Key.Equals(ov.Key) + + // Even if the value is known, it may not be wholly known, and in + // that case it does not serve as a wildcard. + if !eq.IsKnown() || !eq.True() { + return false + } + default: + // Any invalid steps default to evaluating false. + return false + } + } + + return true +} + // GetAttrPath is a convenience method to start a new Path with a GetAttrStep. func GetAttrPath(name string) Path { return Path{}.GetAttr(name) diff --git a/cty/path_test.go b/cty/path_test.go index 9c2472af..09387dd9 100644 --- a/cty/path_test.go +++ b/cty/path_test.go @@ -123,3 +123,180 @@ func TestPathApply(t *testing.T) { }) } } + +func TestPathMatch(t *testing.T) { + tests := []struct { + A, B cty.Path + Match bool + }{ + { + A: nil, + B: nil, + Match: true, + }, + { + A: cty.Path{}, + B: cty.Path{}, + Match: true, + }, + { + A: cty.Path{nil}, + B: cty.Path{cty.GetAttrStep{Name: "attr"}}, + Match: false, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.UnknownVal(cty.String)}, + cty.GetAttrStep{Name: "attr"}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.StringVal("key")}, + cty.GetAttrStep{Name: "attr"}, + }, + Match: true, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.ListVal([]cty.Value{cty.UnknownVal(cty.String)})}, + cty.GetAttrStep{Name: "attr"}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.ListVal([]cty.Value{cty.StringVal("known")})}, + cty.GetAttrStep{Name: "attr"}, + }, + Match: false, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.UnknownVal(cty.String)}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.StringVal("known")}, + cty.GetAttrStep{Name: "attr"}, + }, + Match: true, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.UnknownVal(cty.String)}, + cty.IndexStep{Key: cty.UnknownVal(cty.Number)}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.StringVal("known")}, + cty.IndexStep{Key: cty.NumberIntVal(0)}, + cty.GetAttrStep{Name: "attr"}, + }, + Match: true, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.UnknownVal(cty.String)}, + cty.IndexStep{Key: cty.UnknownVal(cty.Number)}, + cty.GetAttrStep{Name: "attr"}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.StringVal("known")}, + cty.IndexStep{Key: cty.NumberIntVal(0)}, + cty.GetAttrStep{Name: "attr"}, + }, + Match: true, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.UnknownVal(cty.String)}, + cty.IndexStep{Key: cty.UnknownVal(cty.Number)}, + cty.GetAttrStep{Name: "attr"}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.StringVal("known")}, + cty.IndexStep{Key: cty.NumberIntVal(0)}, + cty.GetAttrStep{Name: "other_attr"}, + }, + Match: false, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.UnknownVal(cty.String)}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.NumberIntVal(0)}, + cty.GetAttrStep{Name: "attr"}, + }, + Match: false, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.UnknownVal(cty.String)}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.UnknownVal(cty.String)}, + }, + Match: true, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.StringVal("known")}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.UnknownVal(cty.String)}, + cty.GetAttrStep{Name: "attr"}, + }, + Match: false, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.NumberFloatVal(0)}, + cty.GetAttrStep{Name: "attr"}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.NumberIntVal(0)}, + cty.GetAttrStep{Name: "attr"}, + }, + Match: true, + }, + { + A: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.NumberIntVal(1)}, + cty.GetAttrStep{Name: "attr"}, + }, + B: cty.Path{ + cty.GetAttrStep{Name: "attr"}, + cty.IndexStep{Key: cty.NumberIntVal(0)}, + cty.GetAttrStep{Name: "attr"}, + }, + Match: false, + }, + } + + for i, test := range tests { + t.Run(fmt.Sprintf("%d-%#v", i, test.A), func(t *testing.T) { + if !test.Match == test.A.Matches(test.B) { + t.Fatalf("%#v.Matches(%#v) == %t", test.A, test.B, test.Match) + } + if !test.Match == test.B.Matches(test.A) { + t.Fatalf("%#v.Matches(%#v) == %t", test.B, test.A, test.Match) + } + }) + } +}