| package gstruct |
| |
| import ( |
| "errors" |
| "fmt" |
| "reflect" |
| "runtime/debug" |
| "strings" |
| |
| "github.com/onsi/gomega/format" |
| errorsutil "github.com/onsi/gomega/gstruct/errors" |
| "github.com/onsi/gomega/types" |
| ) |
| |
| //MatchAllFields succeeds if every field of a struct matches the field matcher associated with |
| //it, and every element matcher is matched. |
| // Expect([]string{"a", "b"}).To(MatchAllFields(idFn, gstruct.Fields{ |
| // "a": BeEqual("a"), |
| // "b": BeEqual("b"), |
| // }) |
| func MatchAllFields(fields Fields) types.GomegaMatcher { |
| return &FieldsMatcher{ |
| Fields: fields, |
| } |
| } |
| |
| //MatchFields succeeds if each element of a struct matches the field matcher associated with |
| //it. It can ignore extra fields and/or missing fields. |
| // Expect([]string{"a", "c"}).To(MatchFields(idFn, IgnoreMissing|IgnoreExtra, gstruct.Fields{ |
| // "a": BeEqual("a") |
| // "b": BeEqual("b"), |
| // }) |
| func MatchFields(options Options, fields Fields) types.GomegaMatcher { |
| return &FieldsMatcher{ |
| Fields: fields, |
| IgnoreExtras: options&IgnoreExtras != 0, |
| IgnoreMissing: options&IgnoreMissing != 0, |
| } |
| } |
| |
| type FieldsMatcher struct { |
| // Matchers for each field. |
| Fields Fields |
| |
| // Whether to ignore extra elements or consider it an error. |
| IgnoreExtras bool |
| // Whether to ignore missing elements or consider it an error. |
| IgnoreMissing bool |
| |
| // State. |
| failures []error |
| } |
| |
| // Field name to matcher. |
| type Fields map[string]types.GomegaMatcher |
| |
| func (m *FieldsMatcher) Match(actual interface{}) (success bool, err error) { |
| if reflect.TypeOf(actual).Kind() != reflect.Struct { |
| return false, fmt.Errorf("%v is type %T, expected struct", actual, actual) |
| } |
| |
| m.failures = m.matchFields(actual) |
| if len(m.failures) > 0 { |
| return false, nil |
| } |
| return true, nil |
| } |
| |
| func (m *FieldsMatcher) matchFields(actual interface{}) (errs []error) { |
| val := reflect.ValueOf(actual) |
| typ := val.Type() |
| fields := map[string]bool{} |
| for i := 0; i < val.NumField(); i++ { |
| fieldName := typ.Field(i).Name |
| fields[fieldName] = true |
| |
| err := func() (err error) { |
| // This test relies heavily on reflect, which tends to panic. |
| // Recover here to provide more useful error messages in that case. |
| defer func() { |
| if r := recover(); r != nil { |
| err = fmt.Errorf("panic checking %+v: %v\n%s", actual, r, debug.Stack()) |
| } |
| }() |
| |
| matcher, expected := m.Fields[fieldName] |
| if !expected { |
| if !m.IgnoreExtras { |
| return fmt.Errorf("unexpected field %s: %+v", fieldName, actual) |
| } |
| return nil |
| } |
| |
| var field interface{} |
| if val.Field(i).IsValid() { |
| field = val.Field(i).Interface() |
| } else { |
| field = reflect.Zero(typ.Field(i).Type) |
| } |
| |
| match, err := matcher.Match(field) |
| if err != nil { |
| return err |
| } else if !match { |
| if nesting, ok := matcher.(errorsutil.NestingMatcher); ok { |
| return errorsutil.AggregateError(nesting.Failures()) |
| } |
| return errors.New(matcher.FailureMessage(field)) |
| } |
| return nil |
| }() |
| if err != nil { |
| errs = append(errs, errorsutil.Nest("."+fieldName, err)) |
| } |
| } |
| |
| for field := range m.Fields { |
| if !fields[field] && !m.IgnoreMissing { |
| errs = append(errs, fmt.Errorf("missing expected field %s", field)) |
| } |
| } |
| |
| return errs |
| } |
| |
| func (m *FieldsMatcher) FailureMessage(actual interface{}) (message string) { |
| failures := make([]string, len(m.failures)) |
| for i := range m.failures { |
| failures[i] = m.failures[i].Error() |
| } |
| return format.Message(reflect.TypeOf(actual).Name(), |
| fmt.Sprintf("to match fields: {\n%v\n}\n", strings.Join(failures, "\n"))) |
| } |
| |
| func (m *FieldsMatcher) NegatedFailureMessage(actual interface{}) (message string) { |
| return format.Message(actual, "not to match fields") |
| } |
| |
| func (m *FieldsMatcher) Failures() []error { |
| return m.failures |
| } |