Merge pull request #187 from jraqula/master

allow specifying that a matcher can apply to multiple members of a slice
diff --git a/gstruct/elements.go b/gstruct/elements.go
index 9b6b134..a315fa1 100644
--- a/gstruct/elements.go
+++ b/gstruct/elements.go
@@ -32,10 +32,11 @@
 //  })
 func MatchElements(identifier Identifier, options Options, elements Elements) types.GomegaMatcher {
 	return &ElementsMatcher{
-		Identifier:    identifier,
-		Elements:      elements,
-		IgnoreExtras:  options&IgnoreExtras != 0,
-		IgnoreMissing: options&IgnoreMissing != 0,
+		Identifier:      identifier,
+		Elements:        elements,
+		IgnoreExtras:    options&IgnoreExtras != 0,
+		IgnoreMissing:   options&IgnoreMissing != 0,
+		AllowDuplicates: options&AllowDuplicates != 0,
 	}
 }
 
@@ -52,6 +53,8 @@
 	IgnoreExtras bool
 	// Whether to ignore missing elements or consider it an error.
 	IgnoreMissing bool
+	// Whether to key duplicates when matching IDs.
+	AllowDuplicates bool
 
 	// State.
 	failures []error
@@ -88,10 +91,11 @@
 	for i := 0; i < val.Len(); i++ {
 		element := val.Index(i).Interface()
 		id := m.Identifier(element)
-		// TODO: Add options to ignore & match duplicates.
 		if elements[id] {
-			errs = append(errs, fmt.Errorf("found duplicate element ID %s", id))
-			continue
+			if !m.AllowDuplicates {
+				errs = append(errs, fmt.Errorf("found duplicate element ID %s", id))
+				continue
+			}
 		}
 		elements[id] = true
 
diff --git a/gstruct/elements_test.go b/gstruct/elements_test.go
index 8a63920..8ba78cb 100644
--- a/gstruct/elements_test.go
+++ b/gstruct/elements_test.go
@@ -78,6 +78,65 @@
 		})
 		Ω(allElements).ShouldNot(m, "should run nested matchers")
 	})
+
+	Context("with elements that share a key", func() {
+		nonUniqueID := func(element interface{}) string {
+			return element.(string)[0:1]
+		}
+
+		allElements := []string{"a123", "a213", "b321"}
+		includingBadElements := []string{"a123", "b123", "b5555"}
+		extraElements := []string{"a123", "b1234", "c345"}
+		missingElements := []string{"b123", "b1234", "b1345"}
+
+		It("should strictly allow multiple matches", func() {
+			m := MatchElements(nonUniqueID, AllowDuplicates, Elements{
+				"a": ContainSubstring("1"),
+				"b": ContainSubstring("1"),
+			})
+			Ω(allElements).Should(m, "should match all elements")
+			Ω(includingBadElements).ShouldNot(m, "should reject if a member fails the matcher")
+			Ω(extraElements).ShouldNot(m, "should reject with extra keys")
+			Ω(missingElements).ShouldNot(m, "should reject with missing keys")
+			Ω(nils).ShouldNot(m, "should fail with an uninitialized slice")
+		})
+
+		It("should ignore missing", func() {
+			m := MatchElements(nonUniqueID, AllowDuplicates|IgnoreMissing, Elements{
+				"a": ContainSubstring("1"),
+				"b": ContainSubstring("1"),
+			})
+			Ω(allElements).Should(m, "should match all elements")
+			Ω(includingBadElements).ShouldNot(m, "should reject if a member fails the matcher")
+			Ω(extraElements).ShouldNot(m, "should reject with extra keys")
+			Ω(missingElements).Should(m, "should allow missing keys")
+			Ω(nils).Should(m, "should allow an uninitialized slice")
+		})
+
+		It("should ignore extras", func() {
+			m := MatchElements(nonUniqueID, AllowDuplicates|IgnoreExtras, Elements{
+				"a": ContainSubstring("1"),
+				"b": ContainSubstring("1"),
+			})
+			Ω(allElements).Should(m, "should match all elements")
+			Ω(includingBadElements).ShouldNot(m, "should reject if a member fails the matcher")
+			Ω(extraElements).Should(m, "should allow extra keys")
+			Ω(missingElements).ShouldNot(m, "should reject missing keys")
+			Ω(nils).ShouldNot(m, "should reject an uninitialized slice")
+		})
+
+		It("should ignore missing and extras", func() {
+			m := MatchElements(nonUniqueID, AllowDuplicates|IgnoreExtras|IgnoreMissing, Elements{
+				"a": ContainSubstring("1"),
+				"b": ContainSubstring("1"),
+			})
+			Ω(allElements).Should(m, "should match all elements")
+			Ω(includingBadElements).ShouldNot(m, "should reject if a member fails the matcher")
+			Ω(extraElements).Should(m, "should allow extra keys")
+			Ω(missingElements).Should(m, "should allow missing keys")
+			Ω(nils).Should(m, "should allow an uninitialized slice")
+		})
+	})
 })
 
 func id(element interface{}) string {
diff --git a/gstruct/types.go b/gstruct/types.go
index 0b7a124..48cbbe8 100644
--- a/gstruct/types.go
+++ b/gstruct/types.go
@@ -8,4 +8,8 @@
 	IgnoreExtras Options = 1 << iota
 	//IgnoreMissing tells the matcher to ignore missing elements or fields, rather than triggering a failure.
 	IgnoreMissing
+	//AllowDuplicates tells the matcher to permit multiple members of the slice to produce the same ID when
+	//considered by the indentifier function. All members that map to a given key must still match successfully
+	//with the matcher that is provided for that key.
+	AllowDuplicates
 )