github.com/onsi/gomega@v1.32.0/gstruct/elements.go (about) 1 // untested sections: 6 2 3 package gstruct 4 5 import ( 6 "errors" 7 "fmt" 8 "reflect" 9 "runtime/debug" 10 "strconv" 11 12 "github.com/onsi/gomega/format" 13 errorsutil "github.com/onsi/gomega/gstruct/errors" 14 "github.com/onsi/gomega/types" 15 ) 16 17 //MatchAllElements succeeds if every element of a slice matches the element matcher it maps to 18 //through the id function, and every element matcher is matched. 19 // idFn := func(element interface{}) string { 20 // return fmt.Sprintf("%v", element) 21 // } 22 // 23 // Expect([]string{"a", "b"}).To(MatchAllElements(idFn, Elements{ 24 // "a": Equal("a"), 25 // "b": Equal("b"), 26 // })) 27 func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcher { 28 return &ElementsMatcher{ 29 Identifier: identifier, 30 Elements: elements, 31 } 32 } 33 34 //MatchAllElementsWithIndex succeeds if every element of a slice matches the element matcher it maps to 35 //through the id with index function, and every element matcher is matched. 36 // idFn := func(index int, element interface{}) string { 37 // return strconv.Itoa(index) 38 // } 39 // 40 // Expect([]string{"a", "b"}).To(MatchAllElements(idFn, Elements{ 41 // "0": Equal("a"), 42 // "1": Equal("b"), 43 // })) 44 func MatchAllElementsWithIndex(identifier IdentifierWithIndex, elements Elements) types.GomegaMatcher { 45 return &ElementsMatcher{ 46 Identifier: identifier, 47 Elements: elements, 48 } 49 } 50 51 //MatchElements succeeds if each element of a slice matches the element matcher it maps to 52 //through the id function. It can ignore extra elements and/or missing elements. 53 // idFn := func(element interface{}) string { 54 // return fmt.Sprintf("%v", element) 55 // } 56 // 57 // Expect([]string{"a", "b", "c"}).To(MatchElements(idFn, IgnoreExtras, Elements{ 58 // "a": Equal("a"), 59 // "b": Equal("b"), 60 // })) 61 // Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing, Elements{ 62 // "a": Equal("a"), 63 // "b": Equal("b"), 64 // "c": Equal("c"), 65 // "d": Equal("d"), 66 // })) 67 func MatchElements(identifier Identifier, options Options, elements Elements) types.GomegaMatcher { 68 return &ElementsMatcher{ 69 Identifier: identifier, 70 Elements: elements, 71 IgnoreExtras: options&IgnoreExtras != 0, 72 IgnoreMissing: options&IgnoreMissing != 0, 73 AllowDuplicates: options&AllowDuplicates != 0, 74 } 75 } 76 77 //MatchElementsWithIndex succeeds if each element of a slice matches the element matcher it maps to 78 //through the id with index function. It can ignore extra elements and/or missing elements. 79 // idFn := func(index int, element interface{}) string { 80 // return strconv.Itoa(index) 81 // } 82 // 83 // Expect([]string{"a", "b", "c"}).To(MatchElements(idFn, IgnoreExtras, Elements{ 84 // "0": Equal("a"), 85 // "1": Equal("b"), 86 // })) 87 // Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing, Elements{ 88 // "0": Equal("a"), 89 // "1": Equal("b"), 90 // "2": Equal("c"), 91 // "3": Equal("d"), 92 // })) 93 func MatchElementsWithIndex(identifier IdentifierWithIndex, options Options, elements Elements) types.GomegaMatcher { 94 return &ElementsMatcher{ 95 Identifier: identifier, 96 Elements: elements, 97 IgnoreExtras: options&IgnoreExtras != 0, 98 IgnoreMissing: options&IgnoreMissing != 0, 99 AllowDuplicates: options&AllowDuplicates != 0, 100 } 101 } 102 103 // ElementsMatcher is a NestingMatcher that applies custom matchers to each element of a slice mapped 104 // by the Identifier function. 105 // TODO: Extend this to work with arrays & maps (map the key) as well. 106 type ElementsMatcher struct { 107 // Matchers for each element. 108 Elements Elements 109 // Function mapping an element to the string key identifying its matcher. 110 Identifier Identify 111 112 // Whether to ignore extra elements or consider it an error. 113 IgnoreExtras bool 114 // Whether to ignore missing elements or consider it an error. 115 IgnoreMissing bool 116 // Whether to key duplicates when matching IDs. 117 AllowDuplicates bool 118 119 // State. 120 failures []error 121 } 122 123 // Element ID to matcher. 124 type Elements map[string]types.GomegaMatcher 125 126 // Function for identifying (mapping) elements. 127 type Identifier func(element interface{}) string 128 129 // Calls the underlying fucntion with the provided params. 130 // Identifier drops the index. 131 func (i Identifier) WithIndexAndElement(index int, element interface{}) string { 132 return i(element) 133 } 134 135 // Uses the index and element to generate an element name 136 type IdentifierWithIndex func(index int, element interface{}) string 137 138 // Calls the underlying fucntion with the provided params. 139 // IdentifierWithIndex uses the index. 140 func (i IdentifierWithIndex) WithIndexAndElement(index int, element interface{}) string { 141 return i(index, element) 142 } 143 144 // Interface for identifing the element 145 type Identify interface { 146 WithIndexAndElement(i int, element interface{}) string 147 } 148 149 // IndexIdentity is a helper function for using an index as 150 // the key in the element map 151 func IndexIdentity(index int, _ interface{}) string { 152 return strconv.Itoa(index) 153 } 154 155 func (m *ElementsMatcher) Match(actual interface{}) (success bool, err error) { 156 if reflect.TypeOf(actual).Kind() != reflect.Slice { 157 return false, fmt.Errorf("%v is type %T, expected slice", actual, actual) 158 } 159 160 m.failures = m.matchElements(actual) 161 if len(m.failures) > 0 { 162 return false, nil 163 } 164 return true, nil 165 } 166 167 func (m *ElementsMatcher) matchElements(actual interface{}) (errs []error) { 168 // Provide more useful error messages in the case of a panic. 169 defer func() { 170 if err := recover(); err != nil { 171 errs = append(errs, fmt.Errorf("panic checking %+v: %v\n%s", actual, err, debug.Stack())) 172 } 173 }() 174 175 val := reflect.ValueOf(actual) 176 elements := map[string]bool{} 177 for i := 0; i < val.Len(); i++ { 178 element := val.Index(i).Interface() 179 id := m.Identifier.WithIndexAndElement(i, element) 180 if elements[id] { 181 if !m.AllowDuplicates { 182 errs = append(errs, fmt.Errorf("found duplicate element ID %s", id)) 183 continue 184 } 185 } 186 elements[id] = true 187 188 matcher, expected := m.Elements[id] 189 if !expected { 190 if !m.IgnoreExtras { 191 errs = append(errs, fmt.Errorf("unexpected element %s", id)) 192 } 193 continue 194 } 195 196 match, err := matcher.Match(element) 197 if match { 198 continue 199 } 200 201 if err == nil { 202 if nesting, ok := matcher.(errorsutil.NestingMatcher); ok { 203 err = errorsutil.AggregateError(nesting.Failures()) 204 } else { 205 err = errors.New(matcher.FailureMessage(element)) 206 } 207 } 208 errs = append(errs, errorsutil.Nest(fmt.Sprintf("[%s]", id), err)) 209 } 210 211 for id := range m.Elements { 212 if !elements[id] && !m.IgnoreMissing { 213 errs = append(errs, fmt.Errorf("missing expected element %s", id)) 214 } 215 } 216 217 return errs 218 } 219 220 func (m *ElementsMatcher) FailureMessage(actual interface{}) (message string) { 221 failure := errorsutil.AggregateError(m.failures) 222 return format.Message(actual, fmt.Sprintf("to match elements: %v", failure)) 223 } 224 225 func (m *ElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) { 226 return format.Message(actual, "not to match elements") 227 } 228 229 func (m *ElementsMatcher) Failures() []error { 230 return m.failures 231 }