added yaml matcher as an 'external' matcher
Signed-off-by: Maria Ntalla <mntalla@pivotal.io>
diff --git a/matchers/external/yaml/match_yaml_matcher.go b/matchers/external/yaml/match_yaml_matcher.go
new file mode 100644
index 0000000..a10c558
--- /dev/null
+++ b/matchers/external/yaml/match_yaml_matcher.go
@@ -0,0 +1,99 @@
+package yaml
+
+import (
+ "fmt"
+ "reflect"
+ "strings"
+
+ "github.com/onsi/gomega/format"
+ "gopkg.in/yaml.v2"
+)
+
+func MatchYAML(yaml interface{}) *MatchYAMLMatcher {
+ return &MatchYAMLMatcher{
+ YAMLToMatch: yaml,
+ }
+}
+
+type MatchYAMLMatcher struct {
+ YAMLToMatch interface{}
+}
+
+func (matcher *MatchYAMLMatcher) Match(actual interface{}) (success bool, err error) {
+ actualString, expectedString, err := matcher.toStrings(actual)
+ if err != nil {
+ return false, err
+ }
+
+ var aval interface{}
+ var eval interface{}
+
+ if err := yaml.Unmarshal([]byte(actualString), &aval); err != nil {
+ return false, fmt.Errorf("Actual '%s' should be valid YAML, but it is not.\nUnderlying error:%s", actualString, err)
+ }
+ if err := yaml.Unmarshal([]byte(expectedString), &eval); err != nil {
+ return false, fmt.Errorf("Expected '%s' should be valid YAML, but it is not.\nUnderlying error:%s", expectedString, err)
+ }
+
+ return reflect.DeepEqual(aval, eval), nil
+}
+
+func (matcher *MatchYAMLMatcher) FailureMessage(actual interface{}) (message string) {
+ actualString, expectedString, _ := matcher.toNormalisedStrings(actual)
+ return format.Message(actualString, "to match YAML of", expectedString)
+}
+
+func (matcher *MatchYAMLMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+ actualString, expectedString, _ := matcher.toNormalisedStrings(actual)
+ return format.Message(actualString, "not to match YAML of", expectedString)
+}
+
+func (matcher *MatchYAMLMatcher) toNormalisedStrings(actual interface{}) (actualFormatted, expectedFormatted string, err error) {
+ actualString, expectedString, err := matcher.toStrings(actual)
+ return normalise(actualString), normalise(expectedString), err
+}
+
+func normalise(input string) string {
+ var val interface{}
+ err := yaml.Unmarshal([]byte(input), &val)
+ if err != nil {
+ panic(err) // guarded by Match
+ }
+ output, err := yaml.Marshal(val)
+ if err != nil {
+ panic(err) // guarded by Unmarshal
+ }
+ return strings.TrimSpace(string(output))
+}
+
+func (matcher *MatchYAMLMatcher) toStrings(actual interface{}) (actualFormatted, expectedFormatted string, err error) {
+ actualString, ok := toString(actual)
+ if !ok {
+ return "", "", fmt.Errorf("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n%s", format.Object(actual, 1))
+ }
+ expectedString, ok := toString(matcher.YAMLToMatch)
+ if !ok {
+ return "", "", fmt.Errorf("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n%s", format.Object(matcher.YAMLToMatch, 1))
+ }
+
+ return actualString, expectedString, nil
+}
+
+func toString(a interface{}) (string, bool) {
+ aString, isString := a.(string)
+ if isString {
+ return aString, true
+ }
+
+ aBytes, isBytes := a.([]byte)
+ if isBytes {
+ return string(aBytes), true
+ }
+
+ aStringer, isStringer := a.(fmt.Stringer)
+ if isStringer {
+ return aStringer.String(), true
+ }
+
+ return "", false
+}
diff --git a/matchers/external/yaml/match_yaml_matcher_test.go b/matchers/external/yaml/match_yaml_matcher_test.go
new file mode 100644
index 0000000..0f0f2f8
--- /dev/null
+++ b/matchers/external/yaml/match_yaml_matcher_test.go
@@ -0,0 +1,94 @@
+package yaml_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ . "github.com/onsi/gomega/matchers/external/yaml"
+)
+
+var _ = Describe("MatchYAMLMatcher", func() {
+ Context("When passed stringifiables", func() {
+ It("should succeed if the YAML matches", func() {
+ Expect("---").Should(MatchYAML(""))
+ Expect("a: 1").Should(MatchYAML(`{"a":1}`))
+ Expect("a: 1\nb: 2").Should(MatchYAML(`{"b":2, "a":1}`))
+ })
+
+ It("should explain if the YAML does not match when it should", func() {
+ message := (&MatchYAMLMatcher{YAMLToMatch: "a: 1"}).FailureMessage("b: 2")
+ Expect(message).To(MatchRegexp(`Expected\s+<string>: b: 2\s+to match YAML of\s+<string>: a: 1`))
+ })
+
+ It("should normalise the expected and actual when explaining if the YAML does not match when it should", func() {
+ message := (&MatchYAMLMatcher{YAMLToMatch: "a: 'one'"}).FailureMessage("{b: two}")
+ Expect(message).To(MatchRegexp(`Expected\s+<string>: b: two\s+to match YAML of\s+<string>: a: one`))
+ })
+
+ It("should explain if the YAML matches when it should not", func() {
+ message := (&MatchYAMLMatcher{YAMLToMatch: "a: 1"}).NegatedFailureMessage("a: 1")
+ Expect(message).To(MatchRegexp(`Expected\s+<string>: a: 1\s+not to match YAML of\s+<string>: a: 1`))
+ })
+
+ It("should normalise the expected and actual when explaining if the YAML matches when it should not", func() {
+ message := (&MatchYAMLMatcher{YAMLToMatch: "a: 'one'"}).NegatedFailureMessage("{a: one}")
+ Expect(message).To(MatchRegexp(`Expected\s+<string>: a: one\s+not to match YAML of\s+<string>: a: one`))
+ })
+
+ It("should fail if the YAML does not match", func() {
+ Expect("a: 1").ShouldNot(MatchYAML(`{"b":2, "a":1}`))
+ })
+
+ It("should work with byte arrays", func() {
+ Expect([]byte("a: 1")).Should(MatchYAML([]byte("a: 1")))
+ Expect("a: 1").Should(MatchYAML([]byte("a: 1")))
+ Expect([]byte("a: 1")).Should(MatchYAML("a: 1"))
+ })
+ })
+
+ Context("when the expected is not valid YAML", func() {
+ It("should error and explain why", func() {
+ success, err := (&MatchYAMLMatcher{YAMLToMatch: ""}).Match("good:\nbad")
+ Expect(success).Should(BeFalse())
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(ContainSubstring("Actual 'good:\nbad' should be valid YAML"))
+ })
+ })
+
+ Context("when the actual is not valid YAML", func() {
+ It("should error and explain why", func() {
+ success, err := (&MatchYAMLMatcher{YAMLToMatch: "good:\nbad"}).Match("")
+ Expect(success).Should(BeFalse())
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(ContainSubstring("Expected 'good:\nbad' should be valid YAML"))
+ })
+ })
+
+ Context("when the expected is neither a string nor a stringer nor a byte array", func() {
+ It("should error", func() {
+ success, err := (&MatchYAMLMatcher{YAMLToMatch: 2}).Match("")
+ Expect(success).Should(BeFalse())
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n <int>: 2"))
+
+ success, err = (&MatchYAMLMatcher{YAMLToMatch: nil}).Match("")
+ Expect(success).Should(BeFalse())
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got expected:\n <nil>: nil"))
+ })
+ })
+
+ Context("when the actual is neither a string nor a stringer nor a byte array", func() {
+ It("should error", func() {
+ success, err := (&MatchYAMLMatcher{YAMLToMatch: ""}).Match(2)
+ Expect(success).Should(BeFalse())
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n <int>: 2"))
+
+ success, err = (&MatchYAMLMatcher{YAMLToMatch: ""}).Match(nil)
+ Expect(success).Should(BeFalse())
+ Expect(err).Should(HaveOccurred())
+ Expect(err.Error()).Should(ContainSubstring("MatchYAMLMatcher matcher requires a string, stringer, or []byte. Got actual:\n <nil>: nil"))
+ })
+ })
+})
diff --git a/matchers/external/yaml/matchers_suite_test.go b/matchers/external/yaml/matchers_suite_test.go
new file mode 100644
index 0000000..8e1226a
--- /dev/null
+++ b/matchers/external/yaml/matchers_suite_test.go
@@ -0,0 +1,13 @@
+package yaml_test
+
+import (
+ . "github.com/onsi/ginkgo"
+ . "github.com/onsi/gomega"
+
+ "testing"
+)
+
+func TestMatchers(t *testing.T) {
+ RegisterFailHandler(Fail)
+ RunSpecs(t, "YAML Matcher Suite")
+}