Repository: gavv/httpexpect
Pull request #178 (2022-11-29 08:48:34)

[#169]Feat: add array and object filter
This PR adds the array and object filter method. It accepts a function that does a boolean check. A review will be highly appreciated.

gavv: #169
neverbeenthisweeb: hi @vtech6 , maybe some suggestion

if you want to have this PR as draft, you can have the PR's state as `Draft`

later on, when this PR is _ready_, can change the state to `Ready for review`

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request
vtech6: @gavv I just pushed the changes, hopefully I managed to address all the comments :). Thanks again for the thorough feedback!

Patch:
diff --git a/README.md b/README.md
index 1e357c8..68d805e 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# httpexpect [![GoDev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/gavv/httpexpect/v2) [![Build](https://github.com/gavv/httpexpect/workflows/build/badge.svg)](https://github.com/gavv/httpexpect/actions) [![Coveralls](https://coveralls.io/repos/github/gavv/httpexpect/badge.svg?branch=master)](https://coveralls.io/github/gavv/httpexpect?branch=master) [![GitHub release](https://img.shields.io/github/tag/gavv/httpexpect.svg)](https://github.com/gavv/httpexpect/tags)
+# httpexpect [![GoDev](https://img.shields.io/badge/go.dev-reference-007d9c?logo=go&logoColor=white)](https://pkg.go.dev/github.com/gavv/httpexpect/v2) [![Build](https://github.com/gavv/httpexpect/workflows/build/badge.svg)](https://github.com/gavv/httpexpect/actions) [![Coveralls](https://coveralls.io/repos/github/gavv/httpexpect/badge.svg?branch=master)](https://coveralls.io/github/gavv/httpexpect?branch=master) [![GitHub release](https://img.shields.io/github/tag/gavv/httpexpect.svg)](https://github.com/gavv/httpexpect/tags) [![Discord](https://img.shields.io/discord/1047473005900615780?logo=discord&label=discord&color=blueviolet&logoColor=white)](https://discord.gg/5SCPCuCWA9)
 
 Concise, declarative, and easy to use end-to-end HTTP and REST API testing for Go (golang).
 
@@ -76,6 +76,12 @@ import "gopkg.in/gavv/httpexpect.v2"
 
 Documentation is available on [pkg.go.dev](https://pkg.go.dev/github.com/gavv/httpexpect/v2#section-documentation). It contains an overview and reference.
 
+## Community
+
+Community forum and Q&A board is right on GitHub in [discussions tab](https://github.com/gavv/httpexpect/discussions).
+
+For more interactive discussion, you can join [discord chat](https://discord.gg/5SCPCuCWA9).
+
 ## Examples
 
 See [`_examples`](_examples) directory for complete standalone examples.
diff --git a/array.go b/array.go
index 4c9da7f..7279d5c 100644
--- a/array.go
+++ b/array.go
@@ -913,3 +913,57 @@ func countElement(array []interface{}, element interface{}) int {
 	}
 	return count
 }
+
+// Filter accepts a function that returns a boolean. The function is ran
+// over the array items. If the function returns true, the item passes
+// the filter and is added to the new array of filtered items. If false,
+// the item is skipped (or in other words filtered out). After iterating
+// through all the items of the original array, the new filtered array
+// is returned.
+//
+// If there are any failed assertions in the filtering function, the
+// item is omitted without causing test failure.
+//
+// Example:
+//
+//	array := NewArray([]interface{}{1, 2, "foo", "bar"}
+//	filteredArray := array.Filter(func(index int, value *Value) bool {
+//		value.String().NotEmpty()		//fails on 1 and 2
+//		return value.Raw() != "bar"		//fails on "bar"
+//	})
+//	filteredArray.Equal([]interface{}{"foo"})	//succeeds
+
+func (a *Array) Filter(filter func(index int, value *Value) bool) *Array {
+	a.chain.enter("Filter()")
+	defer a.chain.leave()
+
+	if a.chain.failed() {
+		return newArray(a.chain, nil)
+	}
+
+	if filter == nil {
+		a.chain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected nil function argument"),
+			},
+		})
+	}
+
+	filteredArray := []interface{}{}
+
+	for index, element := range a.value {
+		valueChain := a.chain.clone()
+		valueChain.setFatal(false)
+		chainFailed := false
+		valueChain.setFailCallback(func() {
+			chainFailed = true
+		})
+		valueChain.replace("Filter[%v]", index)
+		if filter(index, newValue(valueChain, element)) && !chainFailed {
+			filteredArray = append(filteredArray, element)
+		}
+	}
+
+	return newArray(a.chain, filteredArray)
+}
diff --git a/object.go b/object.go
index 836333a..19d1304 100644
--- a/object.go
+++ b/object.go
@@ -710,3 +710,60 @@ func checkSubset(outer, inner map[string]interface{}) bool {
 	}
 	return true
 }
+
+// Filter accepts a function that returns a boolean. The function is ran
+// over the object items. If the function returns true, the item passes
+// the filter and is added to the new object of filtered items. If false,
+// the value is skipped (or in other words filtered out). After iterating
+// through all the items of the original object, the new filtered object
+// is returned.
+//
+// If there are any failed assertions in the filtering function, the
+// item is omitted without causing test failure.
+//
+// Example:
+//
+//	object := NewObject(map[string]interface{}{
+//		"foo": "bar",
+//		"baz": 6,
+//		"qux": "quux"})
+//	filteredObject := object.Filter(func(key string, value *Value) bool {
+//		value.String().NotEmpty()		//fails on 6
+//		return value.Raw() != "bar"		//fails on "bar"
+// 	})
+//	filteredObject.Equal(map[string]interface{}{"qux":"quux"})	//succeeds
+
+func (o *Object) Filter(filter func(key string, value *Value) bool) *Object {
+	o.chain.enter("Filter()")
+	defer o.chain.leave()
+
+	if o.chain.failed() {
+		return newObject(o.chain, nil)
+	}
+
+	if filter == nil {
+		o.chain.fail(AssertionFailure{
+			Type: AssertUsage,
+			Errors: []error{
+				errors.New("unexpected nil function argument"),
+			},
+		})
+	}
+
+	filteredObject := make(map[string]interface{})
+
+	for key, element := range o.value {
+		valueChain := o.chain.clone()
+		valueChain.setFatal(false)
+		chainFailed := false
+		valueChain.setFailCallback(func() {
+			chainFailed = true
+		})
+		valueChain.replace("Filter[%q]", key)
+		if filter(key, newValue(valueChain, element)) && !chainFailed {
+			filteredObject[key] = element
+		}
+	}
+
+	return newObject(o.chain, filteredObject)
+}


Test patch:
diff --git a/array_test.go b/array_test.go
index 7069aa2..3b145e2 100644
--- a/array_test.go
+++ b/array_test.go
@@ -40,6 +40,10 @@ func TestArrayFailed(t *testing.T) {
 		value.Every(func(_ int, val *Value) {
 			val.String().NotEmpty()
 		})
+		value.Filter(func(_ int, val *Value) bool {
+			val.String().NotEmpty()
+			return true
+		})
 	}
 
 	t.Run("failed_chain", func(t *testing.T) {
@@ -808,3 +812,72 @@ func TestArrayEvery(t *testing.T) {
 		array.chain.assertFailed(ts)
 	})
 }
+
+func TestArrayFilter(t *testing.T) {
+	t.Run("Filter an array of elements of the same type and validate", func(ts *testing.T) {
+		reporter := newMockReporter(t)
+		array := NewArray(reporter, []interface{}{1, 2, 3, 4, 5, 6})
+		filteredArray := array.Filter(func(index int, value *Value) bool {
+			return value.Raw() != 2.0 && value.Raw() != 5.0
+		})
+		assert.Equal(t, []interface{}{1.0, 3.0, 4.0, 6.0}, filteredArray.Raw())
+		assert.Equal(t, array.Raw(), []interface{}{1.0, 2.0, 3.0, 4.0, 5.0, 6.0})
+
+		array.chain.assertOK(t)
+		filteredArray.chain.assertOK(t)
+	})
+
+	t.Run("Filter throws when an assertion within predicate fails", func(ts *testing.T) {
+		reporter := newMockReporter(t)
+		array := NewArray(reporter, []interface{}{1.0, "foo", "bar", 4.0, "baz", 6.0})
+		filteredArray := array.Filter(func(index int, value *Value) bool {
+			stringifiedValue := value.String().NotEmpty().Raw()
+			return stringifiedValue != "bar"
+		})
+		assert.Equal(t, []interface{}{"foo", "baz"}, filteredArray.Raw())
+		assert.Equal(t, array.Raw(), []interface{}{1.0, "foo", "bar", 4.0, "baz", 6.0})
+
+		array.chain.assertOK(t)
+		filteredArray.chain.assertOK(t)
+	})
+
+	t.Run("Filter an array of different types and validate", func(ts *testing.T) {
+		reporter := newMockReporter(t)
+		array := NewArray(reporter, []interface{}{"foo", "bar", true, 1.0})
+		filteredArray := array.Filter(func(index int, value *Value) bool {
+			return value.Raw() != "bar"
+		})
+		assert.Equal(t, []interface{}{"foo", true, 1.0}, filteredArray.Raw())
+		assert.Equal(t, array.Raw(), []interface{}{"foo", "bar", true, 1.0})
+
+		array.chain.assertOK(t)
+		filteredArray.chain.assertOK(t)
+	})
+
+	t.Run("Filter an empty array", func(ts *testing.T) {
+		reporter := newMockReporter(t)
+		array := NewArray(reporter, []interface{}{})
+		filteredArray := array.Filter(func(index int, value *Value) bool {
+			return false
+		})
+		assert.Equal(t, []interface{}{}, filteredArray.Raw())
+		assert.Equal(t, array.Raw(), []interface{}{})
+
+		array.chain.assertOK(t)
+		filteredArray.chain.assertOK(t)
+	})
+
+	t.Run("Filter returns an empty non-nil array if no items are passed",
+		func(ts *testing.T) {
+			reporter := newMockReporter(t)
+			array := NewArray(reporter, []interface{}{"foo", "bar", true, 1.0})
+			filteredArray := array.Filter(func(index int, value *Value) bool {
+				return false
+			})
+			assert.Equal(t, []interface{}{}, filteredArray.Raw())
+			assert.Equal(t, array.Raw(), []interface{}{"foo", "bar", true, 1.0})
+
+			array.chain.assertOK(t)
+			filteredArray.chain.assertOK(t)
+		})
+}
diff --git a/object_test.go b/object_test.go
index 8df9183..f2ecc9c 100644
--- a/object_test.go
+++ b/object_test.go
@@ -32,6 +32,10 @@ func TestObjectFailed(t *testing.T) {
 		value.Every(func(_ string, value *Value) {
 			value.String().NotEmpty()
 		})
+		value.Filter(func(_ string, value *Value) bool {
+			value.String().NotEmpty()
+			return true
+		})
 	}
 
 	t.Run("failed_chain", func(t *testing.T) {
@@ -826,3 +830,80 @@ func TestObjectEvery(t *testing.T) {
 		assert.Equal(t, 2, invoked)
 	})
 }
+
+func TestObjectFilter(t *testing.T) {
+	t.Run("Filter an object of elements of the same type and validate", func(ts *testing.T) {
+		reporter := newMockReporter(t)
+		object := NewObject(reporter, map[string]interface{}{"foo": "bar",
+			"baz": "qux", "quux": "corge"})
+		filteredObject := object.Filter(func(key string, value *Value) bool {
+			return value.Raw() != "qux" && key != "quux"
+		})
+		assert.Equal(t, map[string]interface{}{"foo": "bar"}, filteredObject.Raw())
+		assert.Equal(t, object.Raw(), map[string]interface{}{"foo": "bar",
+			"baz": "qux", "quux": "corge"})
+
+		filteredObject.chain.assertOK(t)
+		object.chain.assertOK(t)
+	})
+
+	t.Run("Filter throws when an assertion within predicate fails", func(ts *testing.T) {
+		reporter := newMockReporter(t)
+		object := NewObject(reporter, map[string]interface{}{"foo": "bar", "baz": 6.0,
+			"qux": "quux"})
+		filteredObject := object.Filter(func(key string, value *Value) bool {
+			stringifiedValue := value.String().NotEmpty().Raw()
+			return stringifiedValue != "bar"
+		})
+		assert.Equal(t, map[string]interface{}{"qux": "quux"}, filteredObject.Raw())
+		assert.Equal(t, object.Raw(), map[string]interface{}{"foo": "bar", "baz": 6.0,
+			"qux": "quux"})
+
+		filteredObject.chain.assertOK(t)
+		object.chain.assertOK(t)
+	})
+
+	t.Run("Filter an object of different types and validate", func(ts *testing.T) {
+		reporter := newMockReporter(t)
+		object := NewObject(reporter, map[string]interface{}{"foo": "bar",
+			"baz": 3.0, "qux": false})
+		filteredObject := object.Filter(func(key string, value *Value) bool {
+			return value.Raw() != "bar" && value.Raw() != 3.0
+		})
+		assert.Equal(t, map[string]interface{}{"qux": false}, filteredObject.Raw())
+		assert.Equal(t, object.Raw(), map[string]interface{}{"foo": "bar",
+			"baz": 3.0, "qux": false})
+
+		filteredObject.chain.assertOK(t)
+		object.chain.assertOK(t)
+	})
+
+	t.Run("Filter an empty object", func(ts *testing.T) {
+		reporter := newMockReporter(t)
+		object := NewObject(reporter, map[string]interface{}{})
+		filteredObject := object.Filter(func(key string, value *Value) bool {
+			return false
+		})
+		assert.Equal(t, map[string]interface{}{}, filteredObject.Raw())
+
+		filteredObject.chain.assertOK(t)
+		object.chain.assertOK(t)
+	})
+
+	t.Run("Filter should return an empty non-nil object if no items pass",
+		func(ts *testing.T) {
+			reporter := newMockReporter(t)
+			object := NewObject(reporter, map[string]interface{}{"foo": "bar",
+				"baz": "qux", "quux": "corge"})
+			filteredObject := object.Filter(func(key string, value *Value) bool {
+				return false
+			})
+			assert.Equal(t, map[string]interface{}{}, filteredObject.Raw())
+			assert.Equal(t, object.Raw(), map[string]interface{}{"foo": "bar",
+				"baz": "qux", "quux": "corge"})
+
+			filteredObject.chain.assertOK(t)
+			object.chain.assertOK(t)
+		})
+
+}