Merge pull request #108 from jim-slattery-rs/composition

Added Add, Or, Not, WithTransform matchers for composability.
diff --git a/internal/asyncassertion/async_assertion.go b/internal/asyncassertion/async_assertion.go
index 7bbec43..b007383 100644
--- a/internal/asyncassertion/async_assertion.go
+++ b/internal/asyncassertion/async_assertion.go
@@ -95,6 +95,12 @@
 		return true
 	}
 
+	return MatchMayChangeInTheFuture(matcher, value)
+}
+
+//MatchMayChangeInTheFuture is a helper to call MatchMayChangeInTheFuture on an unknown matcher.
+//If matcher implements oracleMatcher, it will call the method. Otherwise just returns true.
+func MatchMayChangeInTheFuture(matcher types.GomegaMatcher, value interface{}) bool {
 	oracleMatcher, ok := matcher.(oracleMatcher)
 	if !ok {
 		return true
diff --git a/matchers.go b/matchers.go
index 3b39677..b6110c4 100644
--- a/matchers.go
+++ b/matchers.go
@@ -343,3 +343,51 @@
 func BeADirectory() types.GomegaMatcher {
 	return &matchers.BeADirectoryMatcher{}
 }
+
+//And succeeds only if all of the given matchers succeed.
+//The matchers are tried in order, and will fail-fast if one doesn't succeed.
+//  Expect("hi").To(And(HaveLen(2), Equal("hi"))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func And(ms ...types.GomegaMatcher) types.GomegaMatcher {
+	return &matchers.AndMatcher{Matchers: ms}
+}
+
+//SatisfyAll is an alias for And().
+//  Ω("hi").Should(SatisfyAll(HaveLen(2), Equal("hi")))
+func SatisfyAll(matchers ...types.GomegaMatcher) types.GomegaMatcher {
+	return And(matchers...)
+}
+
+//Or succeeds if any of the given matchers succeed.
+//The matchers are tried in order and will return immediately upon the first successful match.
+//  Expect("hi").To(Or(HaveLen(3), HaveLen(2))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func Or(ms ...types.GomegaMatcher) types.GomegaMatcher {
+	return &matchers.OrMatcher{Matchers: ms}
+}
+
+//SatisfyAny is an alias for Or().
+//  Expect("hi").SatisfyAny(Or(HaveLen(3), HaveLen(2))
+func SatisfyAny(matchers ...types.GomegaMatcher) types.GomegaMatcher {
+	return Or(matchers...)
+}
+
+//Not negates the given matcher; it succeeds if the given matcher fails.
+//  Expect(1).To(Not(Equal(2))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func Not(matcher types.GomegaMatcher) types.GomegaMatcher {
+	return &matchers.NotMatcher{Matcher: matcher}
+}
+
+//WithTransform applies the `transform` to the actual value and matches it against `matcher`.
+//The given transform must be a function of one parameter that returns one value.
+//  var plus1 = func(i int) int { return i + 1 }
+//  Expect(1).To(WithTransform(plus1, Equal(2))
+//
+//And(), Or(), Not() and WithTransform() allow matchers to be composed into complex expressions.
+func WithTransform(transform interface{}, matcher types.GomegaMatcher) types.GomegaMatcher {
+	return matchers.NewWithTransformMatcher(transform, matcher)
+}
diff --git a/matchers/and.go b/matchers/and.go
new file mode 100644
index 0000000..5257b76
--- /dev/null
+++ b/matchers/and.go
@@ -0,0 +1,63 @@
+package matchers
+
+import (
+	"fmt"
+	"github.com/onsi/gomega/format"
+	"github.com/onsi/gomega/internal/asyncassertion"
+	"github.com/onsi/gomega/types"
+)
+
+type AndMatcher struct {
+	Matchers []types.GomegaMatcher
+
+	// state
+	firstFailedMatcher types.GomegaMatcher
+}
+
+func (m *AndMatcher) Match(actual interface{}) (success bool, err error) {
+	m.firstFailedMatcher = nil
+	for _, matcher := range m.Matchers {
+		success, err := matcher.Match(actual)
+		if !success || err != nil {
+			m.firstFailedMatcher = matcher
+			return false, err
+		}
+	}
+	return true, nil
+}
+
+func (m *AndMatcher) FailureMessage(actual interface{}) (message string) {
+	return m.firstFailedMatcher.FailureMessage(actual)
+}
+
+func (m *AndMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+	// not the most beautiful list of matchers, but not bad either...
+	return format.Message(actual, fmt.Sprintf("To not satisfy all of these matchers: %s", m.Matchers))
+}
+
+func (m *AndMatcher) MatchMayChangeInTheFuture(actual interface{}) bool {
+	/*
+		Example with 3 matchers: A, B, C
+
+		Match evaluates them: T, F, <?>  => F
+		So match is currently F, what should MatchMayChangeInTheFuture() return?
+		Seems like it only depends on B, since currently B MUST change to allow the result to become T
+
+		Match eval: T, T, T  => T
+		So match is currently T, what should MatchMayChangeInTheFuture() return?
+		Seems to depend on ANY of them being able to change to F.
+	*/
+
+	if m.firstFailedMatcher == nil {
+		// so all matchers succeeded.. Any one of them changing would change the result.
+		for _, matcher := range m.Matchers {
+			if asyncassertion.MatchMayChangeInTheFuture(matcher, actual) {
+				return true
+			}
+		}
+		return false // none of were going to change
+	} else {
+		// one of the matchers failed.. it must be able to change in order to affect the result
+		return asyncassertion.MatchMayChangeInTheFuture(m.firstFailedMatcher, actual)
+	}
+}
diff --git a/matchers/and_test.go b/matchers/and_test.go
new file mode 100644
index 0000000..acf778c
--- /dev/null
+++ b/matchers/and_test.go
@@ -0,0 +1,103 @@
+package matchers_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	. "github.com/onsi/gomega/matchers"
+	"github.com/onsi/gomega/types"
+)
+
+// sample data
+var (
+	// example input
+	input = "hi"
+	// some matchers that succeed against the input
+	true1 = HaveLen(2)
+	true2 = Equal("hi")
+	true3 = MatchRegexp("hi")
+	// some matchers that fail against the input.
+	false1 = HaveLen(1)
+	false2 = Equal("hip")
+	false3 = MatchRegexp("hope")
+)
+
+// verifyFailureMessage expects the matcher to fail with the given input, and verifies the failure message.
+func verifyFailureMessage(m types.GomegaMatcher, input string, expectedFailureMsgFragment string) {
+	Expect(m.Match(input)).To(BeFalse())
+	Expect(m.FailureMessage(input)).To(Equal(
+		"Expected\n    <string>: " + input + "\n" + expectedFailureMsgFragment))
+}
+
+var _ = Describe("AndMatcher", func() {
+	It("works with positive cases", func() {
+		Expect(input).To(And())
+		Expect(input).To(And(true1))
+		Expect(input).To(And(true1, true2))
+		Expect(input).To(And(true1, true2, true3))
+
+		// use alias
+		Expect(input).To(SatisfyAll(true1, true2, true3))
+	})
+
+	It("works with negative cases", func() {
+		Expect(input).ToNot(And(false1, false2))
+		Expect(input).ToNot(And(true1, true2, false3))
+		Expect(input).ToNot(And(true1, false2, false3))
+		Expect(input).ToNot(And(false1, true1, true2))
+	})
+
+	Context("failure messages", func() {
+		Context("when match fails", func() {
+			It("gives a descriptive message", func() {
+				verifyFailureMessage(And(false1, true1), input, "to have length 1")
+				verifyFailureMessage(And(true1, false2), input, "to equal\n    <string>: hip")
+				verifyFailureMessage(And(true1, true2, false3), input, "to match regular expression\n    <string>: hope")
+			})
+		})
+
+		Context("when match succeeds, but expected it to fail", func() {
+			It("gives a descriptive message", func() {
+				verifyFailureMessage(Not(And(true1, true2)), input,
+					`To not satisfy all of these matchers: [%!s(*matchers.HaveLenMatcher=&{2}) %!s(*matchers.EqualMatcher=&{hi})]`)
+			})
+		})
+	})
+
+	Context("MatchMayChangeInTheFuture", func() {
+		Context("Match returned false", func() {
+			Context("returns value of the failed matcher", func() {
+				It("false if failed matcher not going to change", func() {
+					// 3 matchers: 1st returns true, 2nd returns false and is not going to change, 3rd is never called
+					m := And(Not(BeNil()), Or(), Equal(1))
+					Expect(m.Match("hi")).To(BeFalse())
+					Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse()) // empty Or() indicates not going to change
+				})
+				It("true if failed matcher indicates it might change", func() {
+					// 3 matchers: 1st returns true, 2nd returns false and "might" change, 3rd is never called
+					m := And(Not(BeNil()), Equal(5), Equal(1))
+					Expect(m.Match("hi")).To(BeFalse())
+					Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // Equal(5) indicates it might change
+				})
+			})
+		})
+		Context("Match returned true", func() {
+			It("returns true if any of the matchers could change", func() {
+				// 3 matchers, all return true, and all could change
+				m := And(Not(BeNil()), Equal("hi"), HaveLen(2))
+				Expect(m.Match("hi")).To(BeTrue())
+				Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // all 3 of these matchers default to 'true'
+			})
+			It("returns false if none of the matchers could change", func() {
+				// empty And() has the property of always matching, and never can change since there are no sub-matchers that could change
+				m := And()
+				Expect(m.Match("anything")).To(BeTrue())
+				Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("anything")).To(BeFalse())
+
+				// And() with 3 sub-matchers that return true, and can't change
+				m = And(And(), And(), And())
+				Expect(m.Match("hi")).To(BeTrue())
+				Expect(m.(*AndMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse()) // the 3 empty And()'s won't change
+			})
+		})
+	})
+})
diff --git a/matchers/not.go b/matchers/not.go
new file mode 100644
index 0000000..6aed858
--- /dev/null
+++ b/matchers/not.go
@@ -0,0 +1,30 @@
+package matchers
+
+import (
+	"github.com/onsi/gomega/internal/asyncassertion"
+	"github.com/onsi/gomega/types"
+)
+
+type NotMatcher struct {
+	Matcher types.GomegaMatcher
+}
+
+func (m *NotMatcher) Match(actual interface{}) (bool, error) {
+	success, err := m.Matcher.Match(actual)
+	if err != nil {
+		return false, err
+	}
+	return !success, nil
+}
+
+func (m *NotMatcher) FailureMessage(actual interface{}) (message string) {
+	return m.Matcher.NegatedFailureMessage(actual) // works beautifully
+}
+
+func (m *NotMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+	return m.Matcher.FailureMessage(actual) // works beautifully
+}
+
+func (m *NotMatcher) MatchMayChangeInTheFuture(actual interface{}) bool {
+	return asyncassertion.MatchMayChangeInTheFuture(m.Matcher, actual) // just return m.Matcher's value
+}
diff --git a/matchers/not_test.go b/matchers/not_test.go
new file mode 100644
index 0000000..b3c1fdb
--- /dev/null
+++ b/matchers/not_test.go
@@ -0,0 +1,57 @@
+package matchers_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	. "github.com/onsi/gomega/matchers"
+)
+
+var _ = Describe("NotMatcher", func() {
+	Context("basic examples", func() {
+		It("works", func() {
+			Expect(input).To(Not(false1))
+			Expect(input).To(Not(Not(true2)))
+			Expect(input).ToNot(Not(true3))
+			Expect(input).ToNot(Not(Not(false1)))
+			Expect(input).To(Not(Not(Not(false2))))
+		})
+	})
+
+	Context("De Morgan's laws", func() {
+		It("~(A && B) == ~A || ~B", func() {
+			Expect(input).To(Not(And(false1, false2)))
+			Expect(input).To(Or(Not(false1), Not(false2)))
+		})
+		It("~(A || B) == ~A && ~B", func() {
+			Expect(input).To(Not(Or(false1, false2)))
+			Expect(input).To(And(Not(false1), Not(false2)))
+		})
+	})
+
+	Context("failure messages are opposite of original matchers' failure messages", func() {
+		Context("when match fails", func() {
+			It("gives a descriptive message", func() {
+				verifyFailureMessage(Not(HaveLen(2)), input, "not to have length 2")
+			})
+		})
+
+		Context("when match succeeds, but expected it to fail", func() {
+			It("gives a descriptive message", func() {
+				verifyFailureMessage(Not(Not(HaveLen(3))), input, "to have length 3")
+			})
+		})
+	})
+
+	Context("MatchMayChangeInTheFuture()", func() {
+		It("Propagates value from wrapped matcher", func() {
+			m := Not(Or()) // an empty Or() always returns false, and indicates it cannot change
+			Expect(m.Match("anything")).To(BeTrue())
+			Expect(m.(*NotMatcher).MatchMayChangeInTheFuture("anything")).To(BeFalse())
+		})
+		It("Defaults to true", func() {
+			m := Not(Equal(1)) // Equal does not have this method
+			Expect(m.Match(2)).To(BeTrue())
+			Expect(m.(*NotMatcher).MatchMayChangeInTheFuture(2)).To(BeTrue()) // defaults to true
+		})
+	})
+})
diff --git a/matchers/or.go b/matchers/or.go
new file mode 100644
index 0000000..29ad5c6
--- /dev/null
+++ b/matchers/or.go
@@ -0,0 +1,66 @@
+package matchers
+
+import (
+	"fmt"
+	"github.com/onsi/gomega/format"
+	"github.com/onsi/gomega/internal/asyncassertion"
+	"github.com/onsi/gomega/types"
+)
+
+type OrMatcher struct {
+	Matchers []types.GomegaMatcher
+
+	// state
+	firstSuccessfulMatcher types.GomegaMatcher
+}
+
+func (m *OrMatcher) Match(actual interface{}) (success bool, err error) {
+	m.firstSuccessfulMatcher = nil
+	for _, matcher := range m.Matchers {
+		success, err := matcher.Match(actual)
+		if err != nil {
+			return false, err
+		}
+		if success {
+			m.firstSuccessfulMatcher = matcher
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+func (m *OrMatcher) FailureMessage(actual interface{}) (message string) {
+	// not the most beautiful list of matchers, but not bad either...
+	return format.Message(actual, fmt.Sprintf("To satisfy at least one of these matchers: %s", m.Matchers))
+}
+
+func (m *OrMatcher) NegatedFailureMessage(actual interface{}) (message string) {
+	return m.firstSuccessfulMatcher.NegatedFailureMessage(actual)
+}
+
+func (m *OrMatcher) MatchMayChangeInTheFuture(actual interface{}) bool {
+	/*
+		Example with 3 matchers: A, B, C
+
+		Match evaluates them: F, T, <?>  => T
+		So match is currently T, what should MatchMayChangeInTheFuture() return?
+		Seems like it only depends on B, since currently B MUST change to allow the result to become F
+
+		Match eval: F, F, F  => F
+		So match is currently F, what should MatchMayChangeInTheFuture() return?
+		Seems to depend on ANY of them being able to change to T.
+	*/
+
+	if m.firstSuccessfulMatcher != nil {
+		// one of the matchers succeeded.. it must be able to change in order to affect the result
+		return asyncassertion.MatchMayChangeInTheFuture(m.firstSuccessfulMatcher, actual)
+	} else {
+		// so all matchers failed.. Any one of them changing would change the result.
+		for _, matcher := range m.Matchers {
+			if asyncassertion.MatchMayChangeInTheFuture(matcher, actual) {
+				return true
+			}
+		}
+		return false // none of were going to change
+	}
+}
diff --git a/matchers/or_test.go b/matchers/or_test.go
new file mode 100644
index 0000000..9589a17
--- /dev/null
+++ b/matchers/or_test.go
@@ -0,0 +1,85 @@
+package matchers_test
+
+import (
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	. "github.com/onsi/gomega/matchers"
+)
+
+var _ = Describe("OrMatcher", func() {
+	It("works with positive cases", func() {
+		Expect(input).To(Or(true1))
+		Expect(input).To(Or(true1, true2))
+		Expect(input).To(Or(true1, false1))
+		Expect(input).To(Or(false1, true2))
+		Expect(input).To(Or(true1, true2, true3))
+		Expect(input).To(Or(true1, true2, false3))
+		Expect(input).To(Or(true1, false2, true3))
+		Expect(input).To(Or(false1, true2, true3))
+		Expect(input).To(Or(true1, false2, false3))
+		Expect(input).To(Or(false1, false2, true3))
+
+		// use alias
+		Expect(input).To(SatisfyAny(false1, false2, true3))
+	})
+
+	It("works with negative cases", func() {
+		Expect(input).ToNot(Or())
+		Expect(input).ToNot(Or(false1))
+		Expect(input).ToNot(Or(false1, false2))
+		Expect(input).ToNot(Or(false1, false2, false3))
+	})
+
+	Context("failure messages", func() {
+		Context("when match fails", func() {
+			It("gives a descriptive message", func() {
+				verifyFailureMessage(Or(false1, false2), input,
+					"To satisfy at least one of these matchers: [%!s(*matchers.HaveLenMatcher=&{1}) %!s(*matchers.EqualMatcher=&{hip})]")
+			})
+		})
+
+		Context("when match succeeds, but expected it to fail", func() {
+			It("gives a descriptive message", func() {
+				verifyFailureMessage(Not(Or(true1, true2)), input, `not to have length 2`)
+			})
+		})
+	})
+
+	Context("MatchMayChangeInTheFuture", func() {
+		Context("Match returned false", func() {
+			It("returns true if any of the matchers could change", func() {
+				// 3 matchers, all return false, and all could change
+				m := Or(BeNil(), Equal("hip"), HaveLen(1))
+				Expect(m.Match("hi")).To(BeFalse())
+				Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // all 3 of these matchers default to 'true'
+			})
+			It("returns false if none of the matchers could change", func() {
+				// empty Or() has the property of never matching, and never can change since there are no sub-matchers that could change
+				m := Or()
+				Expect(m.Match("anything")).To(BeFalse())
+				Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("anything")).To(BeFalse())
+
+				// Or() with 3 sub-matchers that return false, and can't change
+				m = Or(Or(), Or(), Or())
+				Expect(m.Match("hi")).To(BeFalse())
+				Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse()) // the 3 empty Or()'s won't change
+			})
+		})
+		Context("Match returned true", func() {
+			Context("returns value of the successful matcher", func() {
+				It("false if successful matcher not going to change", func() {
+					// 3 matchers: 1st returns false, 2nd returns true and is not going to change, 3rd is never called
+					m := Or(BeNil(), And(), Equal(1))
+					Expect(m.Match("hi")).To(BeTrue())
+					Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeFalse())
+				})
+				It("true if successful matcher indicates it might change", func() {
+					// 3 matchers: 1st returns false, 2nd returns true and "might" change, 3rd is never called
+					m := Or(Not(BeNil()), Equal("hi"), Equal(1))
+					Expect(m.Match("hi")).To(BeTrue())
+					Expect(m.(*OrMatcher).MatchMayChangeInTheFuture("hi")).To(BeTrue()) // Equal("hi") indicates it might change
+				})
+			})
+		})
+	})
+})
diff --git a/matchers/with_transform.go b/matchers/with_transform.go
new file mode 100644
index 0000000..21d9fe0
--- /dev/null
+++ b/matchers/with_transform.go
@@ -0,0 +1,71 @@
+package matchers
+
+import (
+	"fmt"
+	"github.com/onsi/gomega/internal/asyncassertion"
+	"github.com/onsi/gomega/types"
+	"reflect"
+)
+
+type WithTransformMatcher struct {
+	// input
+	Transform interface{} // must be a function of one parameter that returns one value
+	Matcher   types.GomegaMatcher
+
+	// cached value
+	transformArgType reflect.Type
+
+	// state
+	transformedValue interface{}
+}
+
+func NewWithTransformMatcher(transform interface{}, matcher types.GomegaMatcher) *WithTransformMatcher {
+	if transform == nil {
+		panic("transform function cannot be nil")
+	}
+	txType := reflect.TypeOf(transform)
+	if txType.NumIn() != 1 {
+		panic("transform function must have 1 argument")
+	}
+	if txType.NumOut() != 1 {
+		panic("transform function must have 1 return value")
+	}
+
+	return &WithTransformMatcher{
+		Transform:        transform,
+		Matcher:          matcher,
+		transformArgType: reflect.TypeOf(transform).In(0),
+	}
+}
+
+func (m *WithTransformMatcher) Match(actual interface{}) (bool, error) {
+	// return error if actual's type is incompatible with Transform function's argument type
+	actualType := reflect.TypeOf(actual)
+	if !actualType.AssignableTo(m.transformArgType) {
+		return false, fmt.Errorf("Transform function expects '%s' but we have '%s'", m.transformArgType, actualType)
+	}
+
+	// call the Transform function with `actual`
+	fn := reflect.ValueOf(m.Transform)
+	result := fn.Call([]reflect.Value{reflect.ValueOf(actual)})
+	m.transformedValue = result[0].Interface() // expect exactly one value
+
+	return m.Matcher.Match(m.transformedValue)
+}
+
+func (m *WithTransformMatcher) FailureMessage(_ interface{}) (message string) {
+	return m.Matcher.FailureMessage(m.transformedValue)
+}
+
+func (m *WithTransformMatcher) NegatedFailureMessage(_ interface{}) (message string) {
+	return m.Matcher.NegatedFailureMessage(m.transformedValue)
+}
+
+func (m *WithTransformMatcher) MatchMayChangeInTheFuture(_ interface{}) bool {
+	// TODO: Maybe this should always just return true? (Only an issue for non-deterministic transformers.)
+	//
+	// Querying the next matcher is fine if the transformer always will return the same value.
+	// But if the transformer is non-deterministic and returns a different value each time, then there
+	// is no point in querying the next matcher, since it can only comment on the last transformed value.
+	return asyncassertion.MatchMayChangeInTheFuture(m.Matcher, m.transformedValue)
+}
diff --git a/matchers/with_transform_test.go b/matchers/with_transform_test.go
new file mode 100644
index 0000000..2824df9
--- /dev/null
+++ b/matchers/with_transform_test.go
@@ -0,0 +1,101 @@
+package matchers_test
+
+import (
+	"errors"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	. "github.com/onsi/gomega/matchers"
+)
+
+var _ = Describe("WithTransformMatcher", func() {
+
+	var plus1 = func(i int) int { return i + 1 }
+
+	Context("Panic if transform function invalid", func() {
+		panicsWithTransformer := func(transform interface{}) {
+			ExpectWithOffset(1, func() { WithTransform(transform, nil) }).To(Panic())
+		}
+		It("nil", func() {
+			panicsWithTransformer(nil)
+		})
+		Context("Invalid number of args, but correct return value count", func() {
+			It("zero", func() {
+				panicsWithTransformer(func() int { return 5 })
+			})
+			It("two", func() {
+				panicsWithTransformer(func(i, j int) int { return 5 })
+			})
+		})
+		Context("Invalid number of return values, but correct number of arguments", func() {
+			It("zero", func() {
+				panicsWithTransformer(func(i int) {})
+			})
+			It("two", func() {
+				panicsWithTransformer(func(i int) (int, int) { return 5, 6 })
+			})
+		})
+	})
+
+	It("works with positive cases", func() {
+		Expect(1).To(WithTransform(plus1, Equal(2)))
+		Expect(1).To(WithTransform(plus1, WithTransform(plus1, Equal(3))))
+		Expect(1).To(WithTransform(plus1, And(Equal(2), BeNumerically(">", 1))))
+
+		// transform expects custom type
+		type S struct {
+			A int
+			B string
+		}
+		transformer := func(s S) string { return s.B }
+		Expect(S{1, "hi"}).To(WithTransform(transformer, Equal("hi")))
+
+		// transform expects interface
+		errString := func(e error) string { return e.Error() }
+		Expect(errors.New("abc")).To(WithTransform(errString, Equal("abc")))
+	})
+
+	It("works with negative cases", func() {
+		Expect(1).ToNot(WithTransform(plus1, Equal(3)))
+		Expect(1).ToNot(WithTransform(plus1, WithTransform(plus1, Equal(2))))
+	})
+
+	Context("failure messages", func() {
+		Context("when match fails", func() {
+			It("gives a descriptive message", func() {
+				m := WithTransform(plus1, Equal(3))
+				Expect(m.Match(1)).To(BeFalse())
+				Expect(m.FailureMessage(1)).To(Equal("Expected\n    <int>: 2\nto equal\n    <int>: 3"))
+			})
+		})
+
+		Context("when match succeeds, but expected it to fail", func() {
+			It("gives a descriptive message", func() {
+				m := Not(WithTransform(plus1, Equal(3)))
+				Expect(m.Match(2)).To(BeFalse())
+				Expect(m.FailureMessage(2)).To(Equal("Expected\n    <int>: 3\nnot to equal\n    <int>: 3"))
+			})
+		})
+
+		Context("actual value is incompatible with transform function's argument type", func() {
+			It("gracefully fails if transform cannot be performed", func() {
+				m := WithTransform(plus1, Equal(3))
+				result, err := m.Match("hi") // give it a string but transform expects int; doesn't panic
+				Expect(result).To(BeFalse())
+				Expect(err).To(MatchError("Transform function expects 'int' but we have 'string'"))
+			})
+		})
+	})
+
+	Context("MatchMayChangeInTheFuture()", func() {
+		It("Propagates value from wrapped matcher on the transformed value", func() {
+			m := WithTransform(plus1, Or()) // empty Or() always returns false, and indicates it cannot change
+			Expect(m.Match(1)).To(BeFalse())
+			Expect(m.(*WithTransformMatcher).MatchMayChangeInTheFuture(1)).To(BeFalse()) // empty Or() indicates cannot change
+		})
+		It("Defaults to true", func() {
+			m := WithTransform(plus1, Equal(2)) // Equal does not have this method
+			Expect(m.Match(1)).To(BeTrue())
+			Expect(m.(*WithTransformMatcher).MatchMayChangeInTheFuture(1)).To(BeTrue()) // defaults to true
+		})
+	})
+})