elements.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. package gstruct
  2. import (
  3. "errors"
  4. "fmt"
  5. "reflect"
  6. "runtime/debug"
  7. "github.com/onsi/gomega/format"
  8. errorsutil "github.com/onsi/gomega/gstruct/errors"
  9. "github.com/onsi/gomega/types"
  10. )
  11. //MatchAllElements succeeds if every element of a slice matches the element matcher it maps to
  12. //through the id function, and every element matcher is matched.
  13. // idFn := func(element interface{}) string {
  14. // return fmt.Sprintf("%v", element)
  15. // }
  16. //
  17. // Expect([]string{"a", "b"}).To(MatchAllElements(idFn, Elements{
  18. // "a": Equal("a"),
  19. // "b": Equal("b"),
  20. // }))
  21. func MatchAllElements(identifier Identifier, elements Elements) types.GomegaMatcher {
  22. return &ElementsMatcher{
  23. Identifier: identifier,
  24. Elements: elements,
  25. }
  26. }
  27. //MatchElements succeeds if each element of a slice matches the element matcher it maps to
  28. //through the id function. It can ignore extra elements and/or missing elements.
  29. // idFn := func(element interface{}) string {
  30. // return fmt.Sprintf("%v", element)
  31. // }
  32. //
  33. // Expect([]string{"a", "b", "c"}).To(MatchElements(idFn, IgnoreExtras, Elements{
  34. // "a": Equal("a"),
  35. // "b": Equal("b"),
  36. // }))
  37. // Expect([]string{"a", "c"}).To(MatchElements(idFn, IgnoreMissing, Elements{
  38. // "a": Equal("a"),
  39. // "b": Equal("b"),
  40. // "c": Equal("c"),
  41. // "d": Equal("d"),
  42. // }))
  43. func MatchElements(identifier Identifier, options Options, elements Elements) types.GomegaMatcher {
  44. return &ElementsMatcher{
  45. Identifier: identifier,
  46. Elements: elements,
  47. IgnoreExtras: options&IgnoreExtras != 0,
  48. IgnoreMissing: options&IgnoreMissing != 0,
  49. AllowDuplicates: options&AllowDuplicates != 0,
  50. }
  51. }
  52. // ElementsMatcher is a NestingMatcher that applies custom matchers to each element of a slice mapped
  53. // by the Identifier function.
  54. // TODO: Extend this to work with arrays & maps (map the key) as well.
  55. type ElementsMatcher struct {
  56. // Matchers for each element.
  57. Elements Elements
  58. // Function mapping an element to the string key identifying its matcher.
  59. Identifier Identifier
  60. // Whether to ignore extra elements or consider it an error.
  61. IgnoreExtras bool
  62. // Whether to ignore missing elements or consider it an error.
  63. IgnoreMissing bool
  64. // Whether to key duplicates when matching IDs.
  65. AllowDuplicates bool
  66. // State.
  67. failures []error
  68. }
  69. // Element ID to matcher.
  70. type Elements map[string]types.GomegaMatcher
  71. // Function for identifying (mapping) elements.
  72. type Identifier func(element interface{}) string
  73. func (m *ElementsMatcher) Match(actual interface{}) (success bool, err error) {
  74. if reflect.TypeOf(actual).Kind() != reflect.Slice {
  75. return false, fmt.Errorf("%v is type %T, expected slice", actual, actual)
  76. }
  77. m.failures = m.matchElements(actual)
  78. if len(m.failures) > 0 {
  79. return false, nil
  80. }
  81. return true, nil
  82. }
  83. func (m *ElementsMatcher) matchElements(actual interface{}) (errs []error) {
  84. // Provide more useful error messages in the case of a panic.
  85. defer func() {
  86. if err := recover(); err != nil {
  87. errs = append(errs, fmt.Errorf("panic checking %+v: %v\n%s", actual, err, debug.Stack()))
  88. }
  89. }()
  90. val := reflect.ValueOf(actual)
  91. elements := map[string]bool{}
  92. for i := 0; i < val.Len(); i++ {
  93. element := val.Index(i).Interface()
  94. id := m.Identifier(element)
  95. if elements[id] {
  96. if !m.AllowDuplicates {
  97. errs = append(errs, fmt.Errorf("found duplicate element ID %s", id))
  98. continue
  99. }
  100. }
  101. elements[id] = true
  102. matcher, expected := m.Elements[id]
  103. if !expected {
  104. if !m.IgnoreExtras {
  105. errs = append(errs, fmt.Errorf("unexpected element %s", id))
  106. }
  107. continue
  108. }
  109. match, err := matcher.Match(element)
  110. if match {
  111. continue
  112. }
  113. if err == nil {
  114. if nesting, ok := matcher.(errorsutil.NestingMatcher); ok {
  115. err = errorsutil.AggregateError(nesting.Failures())
  116. } else {
  117. err = errors.New(matcher.FailureMessage(element))
  118. }
  119. }
  120. errs = append(errs, errorsutil.Nest(fmt.Sprintf("[%s]", id), err))
  121. }
  122. for id := range m.Elements {
  123. if !elements[id] && !m.IgnoreMissing {
  124. errs = append(errs, fmt.Errorf("missing expected element %s", id))
  125. }
  126. }
  127. return errs
  128. }
  129. func (m *ElementsMatcher) FailureMessage(actual interface{}) (message string) {
  130. failure := errorsutil.AggregateError(m.failures)
  131. return format.Message(actual, fmt.Sprintf("to match elements: %v", failure))
  132. }
  133. func (m *ElementsMatcher) NegatedFailureMessage(actual interface{}) (message string) {
  134. return format.Message(actual, "not to match elements")
  135. }
  136. func (m *ElementsMatcher) Failures() []error {
  137. return m.failures
  138. }