elements.go 4.5 KB

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