Skip to content

Commit

Permalink
feature: add index to gstruct element func (#419)
Browse files Browse the repository at this point in the history
  • Loading branch information
zach-source committed Mar 3, 2021
1 parent 3c60a15 commit 334e00d
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 2 deletions.
74 changes: 72 additions & 2 deletions gstruct/elements.go
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"reflect"
"runtime/debug"
"strconv"

"github.com/onsi/gomega/format"
errorsutil "github.com/onsi/gomega/gstruct/errors"
Expand All @@ -30,6 +31,23 @@ func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatc
}
}

//MatchAllElementsWithIndex succeeds if every element of a slice matches the element matcher it maps to
//through the id with index function, and every element matcher is matched.
// idFn := func(index int, element interface{}) string {
// return strconv.Itoa(index)
// }
//
// Expect([]string{"a", "b"}).To(MatchAllElements(idFn, Elements{
// "0": Equal("a"),
// "1": Equal("b"),
// }))
func MatchAllElementsWithIndex(identifier IdentifierWithIndex, elements Elements) types.GomegaMatcher {
return &ElementsMatcher{
Identifier: identifier,
Elements: elements,
}
}

//MatchElements succeeds if each element of a slice matches the element matcher it maps to
//through the id function. It can ignore extra elements and/or missing elements.
// idFn := func(element interface{}) string {
Expand All @@ -56,14 +74,40 @@ func MatchElements(identifier Identifier, options Options, elements Elements) ty
}
}

//MatchElementsWithIndex succeeds if each element of a slice matches the element matcher it maps to
//through the id with index function. It can ignore extra elements and/or missing elements.
// idFn := func(index int, element interface{}) string {
// return strconv.Itoa(index)
// }
//
// Expect([]string{"a", "b", "c"}).To(MatchElements(idFn, IgnoreExtras, Elements{
// "0": Equal("a"),
// "1": Equal("b"),
// }))
// Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing, Elements{
// "0": Equal("a"),
// "1": Equal("b"),
// "2": Equal("c"),
// "3": Equal("d"),
// }))
func MatchElementsWithIndex(identifier IdentifierWithIndex, options Options, elements Elements) types.GomegaMatcher {
return &ElementsMatcher{
Identifier: identifier,
Elements: elements,
IgnoreExtras: options&IgnoreExtras != 0,
IgnoreMissing: options&IgnoreMissing != 0,
AllowDuplicates: options&AllowDuplicates != 0,
}
}

// ElementsMatcher is a NestingMatcher that applies custom matchers to each element of a slice mapped
// by the Identifier function.
// TODO: Extend this to work with arrays & maps (map the key) as well.
type ElementsMatcher struct {
// Matchers for each element.
Elements Elements
// Function mapping an element to the string key identifying its matcher.
Identifier Identifier
Identifier Identify

// Whether to ignore extra elements or consider it an error.
IgnoreExtras bool
Expand All @@ -82,6 +126,32 @@ type Elements map[string]types.GomegaMatcher
// Function for identifying (mapping) elements.
type Identifier func(element interface{}) string

// Calls the underlying fucntion with the provided params.
// Identifier drops the index.
func (i Identifier) WithIndexAndElement(index int, element interface{}) string {
return i(element)
}

// Uses the index and element to generate an element name
type IdentifierWithIndex func(index int, element interface{}) string

// Calls the underlying fucntion with the provided params.
// IdentifierWithIndex uses the index.
func (i IdentifierWithIndex) WithIndexAndElement(index int, element interface{}) string {
return i(index, element)
}

// Interface for identifing the element
type Identify interface {
WithIndexAndElement(i int, element interface{}) string
}

// IndexIdentity is a helper function for using an index as
// the key in the element map
func IndexIdentity(index int, _ interface{}) string {
return strconv.Itoa(index)
}

func (m *ElementsMatcher) Match(actual interface{}) (success bool, err error) {
if reflect.TypeOf(actual).Kind() != reflect.Slice {
return false, fmt.Errorf("%v is type %T, expected slice", actual, actual)
Expand All @@ -106,7 +176,7 @@ func (m *ElementsMatcher) matchElements(actual interface{}) (errs []error) {
elements := map[string]bool{}
for i := 0; i < val.Len(); i++ {
element := val.Index(i).Interface()
id := m.Identifier(element)
id := m.Identifier.WithIndexAndElement(i, element)
if elements[id] {
if !m.AllowDuplicates {
errs = append(errs, fmt.Errorf("found duplicate element ID %s", id))
Expand Down
32 changes: 32 additions & 0 deletions gstruct/elements_test.go
Expand Up @@ -137,6 +137,38 @@ var _ = Describe("Slice", func() {
Expect(nils).Should(m, "should allow an uninitialized slice")
})
})

Context("with index identifier", func() {
allElements := []string{"a", "b"}
missingElements := []string{"a"}
extraElements := []string{"a", "b", "c"}
duplicateElements := []string{"a", "a", "b"}
empty := []string{}
var nils []string

It("should use index", func() {
m := MatchAllElementsWithIndex(IndexIdentity, Elements{
"0": Equal("a"),
"1": Equal("b"),
})
Expect(allElements).Should(m, "should match all elements")
Expect(missingElements).ShouldNot(m, "should fail with missing elements")
Expect(extraElements).ShouldNot(m, "should fail with extra elements")
Expect(duplicateElements).ShouldNot(m, "should fail with duplicate elements")
Expect(nils).ShouldNot(m, "should fail with an uninitialized slice")

m = MatchAllElementsWithIndex(IndexIdentity, Elements{
"0": Equal("a"),
"1": Equal("fail"),
})
Expect(allElements).ShouldNot(m, "should run nested matchers")

m = MatchAllElementsWithIndex(IndexIdentity, Elements{})
Expect(empty).Should(m, "should handle empty slices")
Expect(allElements).ShouldNot(m, "should handle only empty slices")
Expect(nils).Should(m, "should handle nil slices")
})
})
})

func id(element interface{}) string {
Expand Down

0 comments on commit 334e00d

Please sign in to comment.