package gstruct import ( "errors" "fmt" "reflect" "runtime/debug" "github.com/onsi/gomega/format" errorsutil "github.com/onsi/gomega/gstruct/errors" "github.com/onsi/gomega/types" ) //MatchAllElements succeeds if every element of a slice matches the element matcher it maps to //through the id function, and every element matcher is matched. // Expect([]string{"a", "b"}).To(MatchAllElements(idFn, matchers.Elements{ // "a": BeEqual("a"), // "b": BeEqual("b"), // }) func MatchAllElements(identifier Identifier, 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. // Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing|IgnoreExtra, matchers.Elements{ // "a": BeEqual("a") // "b": BeEqual("b"), // }) func MatchElements(identifier Identifier, 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 // Whether to ignore extra elements or consider it an error. IgnoreExtras bool // Whether to ignore missing elements or consider it an error. IgnoreMissing bool // Whether to key duplicates when matching IDs. AllowDuplicates bool // State. failures []error } // Element ID to matcher. type Elements map[string]types.GomegaMatcher // Function for identifying (mapping) elements. type Identifier func(element interface{}) string 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) } m.failures = m.matchElements(actual) if len(m.failures) > 0 { return false, nil } return true, nil } func (m *ElementsMatcher) matchElements(actual interface{}) (errs []error) { // Provide more useful error messages in the case of a panic. defer func() { if err := recover(); err != nil { errs = append(errs, fmt.Errorf("panic checking %+v: %v\n%s", actual, err, debug.Stack())) } }() val := reflect.ValueOf(actual) elements := map[string]bool{} for i := 0; i < val.Len(); i++ { element := val.Index(i).Interface() id := m.Identifier(element) if elements[id] { if !m.AllowDuplicates { errs = append(errs, fmt.Errorf("found duplicate element ID %s", id)) continue } } elements[id] = true matcher, expected := m.Elements[id] if !expected { if !m.IgnoreExtras { errs = append(errs, fmt.Errorf("unexpected element %s", id)) } continue } match, err := matcher.Match(element) if match { continue } if err == nil { if nesting, ok := matcher.(errorsutil.NestingMatcher); ok { err = errorsutil.AggregateError(nesting.Failures()) } else { err = errors.New(matcher.FailureMessage(element)) } } errs = append(errs, errorsutil.Nest(fmt.Sprintf("[%s]", id), err)) } for id := range m.Elements { if !elements[id] && !m.IgnoreMissing { errs = append(errs, fmt.Errorf("missing expected element %s", id)) } } return errs } func (m *ElementsMatcher) FailureMessage(actual interface{}) (message string) { failure := errorsutil.AggregateError(m.failures) return format.Message(actual, fmt.Sprintf("to match elements: %v", failure)) } func (m *ElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) { return format.Message(actual, "not to match elements") } func (m *ElementsMatcher) Failures() []error { return m.failures }