Skip to content

Commit

Permalink
implement Path.Matches
Browse files Browse the repository at this point in the history
Paths comparisons done with reflect.DeepEqual may erroneously return
false for numeric IndexSteps, due to the differences in the internal
values of the big.Float.

Add a Path.Matches method to ensure all IndexSteps compare correctly.

This also allows us to implement the unknown IndexStep functionality,
which is documented that an unknown value can match any value of the
same type. Shorter paths ending in an unknown value will match against
the same length subset of the longer path.
  • Loading branch information
jbardin committed Jul 5, 2019
1 parent 6fd39ad commit d62f38c
Show file tree
Hide file tree
Showing 2 changed files with 248 additions and 0 deletions.
71 changes: 71 additions & 0 deletions cty/path.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
177 changes: 177 additions & 0 deletions cty/path_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
})
}
}

0 comments on commit d62f38c

Please sign in to comment.