github.com/onsi/gomega@v1.32.0/gstruct/fields.go (about) 1 // untested sections: 6 2 3 package gstruct 4 5 import ( 6 "errors" 7 "fmt" 8 "reflect" 9 "runtime/debug" 10 "strings" 11 12 "github.com/onsi/gomega/format" 13 errorsutil "github.com/onsi/gomega/gstruct/errors" 14 "github.com/onsi/gomega/types" 15 ) 16 17 //MatchAllFields succeeds if every field of a struct matches the field matcher associated with 18 //it, and every element matcher is matched. 19 // actual := struct{ 20 // A int 21 // B []bool 22 // C string 23 // }{ 24 // A: 5, 25 // B: []bool{true, false}, 26 // C: "foo", 27 // } 28 // 29 // Expect(actual).To(MatchAllFields(Fields{ 30 // "A": Equal(5), 31 // "B": ConsistOf(true, false), 32 // "C": Equal("foo"), 33 // })) 34 func MatchAllFields(fields Fields) types.GomegaMatcher { 35 return &FieldsMatcher{ 36 Fields: fields, 37 } 38 } 39 40 //MatchFields succeeds if each element of a struct matches the field matcher associated with 41 //it. It can ignore extra fields and/or missing fields. 42 // actual := struct{ 43 // A int 44 // B []bool 45 // C string 46 // }{ 47 // A: 5, 48 // B: []bool{true, false}, 49 // C: "foo", 50 // } 51 // 52 // Expect(actual).To(MatchFields(IgnoreExtras, Fields{ 53 // "A": Equal(5), 54 // "B": ConsistOf(true, false), 55 // })) 56 // Expect(actual).To(MatchFields(IgnoreMissing, Fields{ 57 // "A": Equal(5), 58 // "B": ConsistOf(true, false), 59 // "C": Equal("foo"), 60 // "D": Equal("extra"), 61 // })) 62 func MatchFields(options Options, fields Fields) types.GomegaMatcher { 63 return &FieldsMatcher{ 64 Fields: fields, 65 IgnoreExtras: options&IgnoreExtras != 0, 66 IgnoreMissing: options&IgnoreMissing != 0, 67 } 68 } 69 70 type FieldsMatcher struct { 71 // Matchers for each field. 72 Fields Fields 73 74 // Whether to ignore extra elements or consider it an error. 75 IgnoreExtras bool 76 // Whether to ignore missing elements or consider it an error. 77 IgnoreMissing bool 78 79 // State. 80 failures []error 81 } 82 83 // Field name to matcher. 84 type Fields map[string]types.GomegaMatcher 85 86 func (m *FieldsMatcher) Match(actual interface{}) (success bool, err error) { 87 if reflect.TypeOf(actual).Kind() != reflect.Struct { 88 return false, fmt.Errorf("%v is type %T, expected struct", actual, actual) 89 } 90 91 m.failures = m.matchFields(actual) 92 if len(m.failures) > 0 { 93 return false, nil 94 } 95 return true, nil 96 } 97 98 func (m *FieldsMatcher) matchFields(actual interface{}) (errs []error) { 99 val := reflect.ValueOf(actual) 100 typ := val.Type() 101 fields := map[string]bool{} 102 for i := 0; i < val.NumField(); i++ { 103 fieldName := typ.Field(i).Name 104 fields[fieldName] = true 105 106 err := func() (err error) { 107 // This test relies heavily on reflect, which tends to panic. 108 // Recover here to provide more useful error messages in that case. 109 defer func() { 110 if r := recover(); r != nil { 111 err = fmt.Errorf("panic checking %+v: %v\n%s", actual, r, debug.Stack()) 112 } 113 }() 114 115 matcher, expected := m.Fields[fieldName] 116 if !expected { 117 if !m.IgnoreExtras { 118 return fmt.Errorf("unexpected field %s: %+v", fieldName, actual) 119 } 120 return nil 121 } 122 123 field := val.Field(i).Interface() 124 125 match, err := matcher.Match(field) 126 if err != nil { 127 return err 128 } else if !match { 129 if nesting, ok := matcher.(errorsutil.NestingMatcher); ok { 130 return errorsutil.AggregateError(nesting.Failures()) 131 } 132 return errors.New(matcher.FailureMessage(field)) 133 } 134 return nil 135 }() 136 if err != nil { 137 errs = append(errs, errorsutil.Nest("."+fieldName, err)) 138 } 139 } 140 141 for field := range m.Fields { 142 if !fields[field] && !m.IgnoreMissing { 143 errs = append(errs, fmt.Errorf("missing expected field %s", field)) 144 } 145 } 146 147 return errs 148 } 149 150 func (m *FieldsMatcher) FailureMessage(actual interface{}) (message string) { 151 failures := make([]string, len(m.failures)) 152 for i := range m.failures { 153 failures[i] = m.failures[i].Error() 154 } 155 return format.Message(reflect.TypeOf(actual).Name(), 156 fmt.Sprintf("to match fields: {\n%v\n}\n", strings.Join(failures, "\n"))) 157 } 158 159 func (m *FieldsMatcher) NegatedFailureMessage(actual interface{}) (message string) { 160 return format.Message(actual, "not to match fields") 161 } 162 163 func (m *FieldsMatcher) Failures() []error { 164 return m.failures 165 }