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")
+}