From 275c3004538c710163cad4b2b5d81cd162262d0d Mon Sep 17 00:00:00 2001 From: Gian <47775302+gpunto@users.noreply.github.com> Date: Fri, 29 Aug 2025 12:56:47 +0200 Subject: [PATCH] Add sort and type-safe filters --- .../android/core/api/filter/Filter.kt | 48 ++++++ .../android/core/api/filter/FilterField.kt | 27 +++ .../android/core/api/filter/Filters.kt | 154 ++++++++++++++++++ .../getstream/android/core/api/filter/Sort.kt | 35 ++++ .../android/core/api/filter/SortComparator.kt | 130 +++++++++++++++ .../android/core/api/filter/SortDirection.kt | 29 ++++ .../android/core/api/filter/SortField.kt | 50 ++++++ .../core/internal/filter/FilterOperator.kt | 63 +++++++ .../core/internal/filter/SortFieldImpl.kt | 29 ++++ .../core/api/filter/FilterToRequestTest.kt | 149 +++++++++++++++++ 10 files changed, 714 insertions(+) create mode 100644 stream-android-core/src/main/java/io/getstream/android/core/api/filter/Filter.kt create mode 100644 stream-android-core/src/main/java/io/getstream/android/core/api/filter/FilterField.kt create mode 100644 stream-android-core/src/main/java/io/getstream/android/core/api/filter/Filters.kt create mode 100644 stream-android-core/src/main/java/io/getstream/android/core/api/filter/Sort.kt create mode 100644 stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortComparator.kt create mode 100644 stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortDirection.kt create mode 100644 stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortField.kt create mode 100644 stream-android-core/src/main/java/io/getstream/android/core/internal/filter/FilterOperator.kt create mode 100644 stream-android-core/src/main/java/io/getstream/android/core/internal/filter/SortFieldImpl.kt create mode 100644 stream-android-core/src/test/java/io/getstream/android/core/api/filter/FilterToRequestTest.kt diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/filter/Filter.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/Filter.kt new file mode 100644 index 0000000..f84cd5e --- /dev/null +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/Filter.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-core-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android.core.api.filter + +import io.getstream.android.core.annotations.StreamInternalApi +import io.getstream.android.core.annotations.StreamPublishedApi +import io.getstream.android.core.internal.filter.FilterOperator + +/** + * Base interface for filters used in Stream API operations. + * + * Filters are used to specify criteria for querying and retrieving data from Stream services. Each + * filter implementation defines specific matching logic for different comparison operations. + */ +@StreamPublishedApi public sealed interface Filter + +internal data class BinaryOperationFilter( + val operator: FilterOperator, + val field: F, + val value: V, +) : Filter + +internal data class CollectionOperationFilter( + internal val operator: FilterOperator, + val filters: Set>, +) : Filter + +/** Converts a [Filter] instance to a request map suitable for API queries. */ +@StreamInternalApi +public fun Filter<*>.toRequest(): Map = + when (this) { + is BinaryOperationFilter<*, *> -> mapOf(field.remote to mapOf(operator.remote to value)) + is CollectionOperationFilter<*> -> + mapOf(operator.remote to filters.map(Filter<*>::toRequest)) + } diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/filter/FilterField.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/FilterField.kt new file mode 100644 index 0000000..3365360 --- /dev/null +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/FilterField.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-core-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android.core.api.filter + +/** + * Interface representing a field that can be used in filters for querying data from the Stream API. + * + * Implementations of this interface should provide [remote], which is the name of the field as + * expected by the Stream API. + */ +public interface FilterField { + /** The name of this field as expected by the Stream API. */ + public val remote: String +} diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/filter/Filters.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/Filters.kt new file mode 100644 index 0000000..7f0402b --- /dev/null +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/Filters.kt @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-core-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android.core.api.filter + +import io.getstream.android.core.internal.filter.FilterOperator + +/** Utility class for building filters. */ +public object Filters { + /** + * Creates a filter that combines multiple filters with a logical AND operation. + * + * @param filters The filters to combine. + * @return A filter that matches when all provided filters match. + */ + public fun and(vararg filters: Filter): Filter = + CollectionOperationFilter(FilterOperator.AND, filters.toSet()) + + /** + * Creates a filter that combines multiple filters with a logical OR operation. + * + * @param filters The filters to combine. + * @return A filter that matches when any of the specified filters match. + */ + public fun or(vararg filters: Filter): Filter = + CollectionOperationFilter(FilterOperator.OR, filters.toSet()) +} + +/** + * Creates a filter that checks if this field equals a specific value. + * + * @param value The value to check equality against. + * @return A filter that matches when this field equals the specified value. + */ +public fun F.equal(value: Any): Filter = + BinaryOperationFilter(FilterOperator.EQUAL, this, value) + +/** + * Creates a filter that checks if this field is greater than a specific value. + * + * @param value The value to check against. + * @return A filter that matches when this field is greater than the specified value. + */ +public fun F.greater(value: Any): Filter = + BinaryOperationFilter(FilterOperator.GREATER, this, value) + +/** + * Creates a filter that checks if this field is greater than or equal to a specific value. + * + * @param value The value to check against. + * @return A filter that matches when this field is greater than or equal to the specified value. + */ +public fun F.greaterOrEqual(value: Any): Filter = + BinaryOperationFilter(FilterOperator.GREATER_OR_EQUAL, this, value) + +/** + * Creates a filter that checks if this field is less than a specific value. + * + * @param value The value to check against. + * @return A filter that matches when this field is less than the specified value. + */ +public fun F.less(value: Any): Filter = + BinaryOperationFilter(FilterOperator.LESS, this, value) + +/** + * Creates a filter that checks if this field is less than or equal to a specific value. + * + * @param value The value to check against. + * @return A filter that matches when this field is less than or equal to the specified value. + */ +public fun F.lessOrEqual(value: Any): Filter = + BinaryOperationFilter(FilterOperator.LESS_OR_EQUAL, this, value) + +/** + * Creates a filter that checks if this field's value is in a specific list of values. + * + * @param values The list of values to check against. + * @return A filter that matches when this field's value is in the specified list. + */ +public fun F.`in`(values: List): Filter = + BinaryOperationFilter(FilterOperator.IN, this, values.toSet()) + +/** + * Creates a filter that checks if this field's value is in a specific set of values. + * + * @param values The values to check against. + * @return A filter that matches when this field's value is in the specified values. + */ +public fun F.`in`(vararg values: Any): Filter = + BinaryOperationFilter(FilterOperator.IN, this, values.toSet()) + +/** + * Creates a filter that performs a full-text query on this field. + * + * @param value The query string to search for. + * @return A filter that matches based on the full-text query. + */ +public fun F.query(value: String): Filter = + BinaryOperationFilter(FilterOperator.QUERY, this, value) + +/** + * Creates a filter that performs autocomplete matching on this field. + * + * @param value The string to autocomplete against. + * @return A filter that matches based on autocomplete functionality. + */ +public fun F.autocomplete(value: String): Filter = + BinaryOperationFilter(FilterOperator.AUTOCOMPLETE, this, value) + +/** + * Creates a filter that checks if this field exists. + * + * @return A filter that matches when this field exists. + */ +public fun F.exists(): Filter = + BinaryOperationFilter(FilterOperator.EXISTS, this, true) + +/** + * Creates a filter that checks if this field does not exist. + * + * @return A filter that matches when this field does not exist. + */ +public fun F.doesNotExist(): Filter = + BinaryOperationFilter(FilterOperator.EXISTS, this, false) + +/** + * Creates a filter that checks if this field contains a specific value. + * + * @param value The value to check for within this field. + * @return A filter that matches when this field contains the specified value. + */ +public fun F.contains(value: Any): Filter = + BinaryOperationFilter(FilterOperator.CONTAINS, this, value) + +/** + * Creates a filter that checks if a specific path exists within this field. + * + * @param value The path to check for existence. + * @return A filter that matches when the specified path exists in this field. + */ +public fun F.pathExists(value: String): Filter = + BinaryOperationFilter(FilterOperator.PATH_EXISTS, this, value) diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/filter/Sort.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/Sort.kt new file mode 100644 index 0000000..640392a --- /dev/null +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/Sort.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-core-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android.core.api.filter + +/** + * A sort configuration that combines a sort field with a direction. + * + * This class represents a complete sort specification that can be applied to collections of the + * associated model type. It provides both local sorting capabilities and the ability to generate + * remote API request parameters. + */ +public open class Sort(public val field: SortField, public val direction: SortDirection) : + Comparator { + + /** Converts this sort configuration to a DTO map for API requests. */ + public fun toDto(): Map = + mapOf("field" to field.remote, "direction" to direction.value) + + override fun compare(o1: T?, o2: T?): Int { + return field.comparator.compare(o1, o2, direction) + } +} diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortComparator.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortComparator.kt new file mode 100644 index 0000000..f8431a2 --- /dev/null +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortComparator.kt @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-core-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android.core.api.filter + +import io.getstream.android.core.annotations.StreamInternalApi + +/** + * A comparator that can sort model instances by extracting comparable values. + * + * This class provides the foundation for local sorting operations by wrapping a lambda that + * extracts comparable values from model instances. It handles the comparison logic and direction + * handling internally. + * + * @param T The type of the model instances to be compared. + * @param V The type of the comparable value extracted from the model instances. + * @property value A lambda that extracts a comparable value from a model instance. + */ +public class SortComparator>(public val value: (T) -> V) { + + /** + * Compares two model instances using the extracted values and sort direction. + * + * @param lhs The first model instance to compare + * @param rhs The second model instance to compare + * @param direction The direction of the sort + * @return A comparison result indicating the relative ordering + */ + public fun compare(lhs: T?, rhs: T?, direction: SortDirection): Int { + val value1 = lhs?.let(value) + val value2 = rhs?.let(value) + + return when { + value1 == null && value2 == null -> 0 + value1 == null -> -direction.value + value2 == null -> direction.value + else -> value1.compareTo(value2) * direction.value + } + } + + /** + * Converts this comparator to a type-erased version. + * + * @return An AnySortComparator that wraps this comparator + */ + public fun toAny(): AnySortComparator { + return AnySortComparator(this) + } +} + +/** + * A type-erased wrapper for sort comparators that can work with any model type. + * + * This class provides a way to store and use sort comparators without knowing their specific + * generic type parameters. It's useful for creating collections of different sort configurations + * that can all work with the same model type. + * + * Type erased type avoids making SortField generic while keeping the underlying value type intact + * (no runtime type checks while sorting). + */ +public class AnySortComparator(private val compare: (T?, T?, SortDirection) -> Int) { + + /** + * Creates a type-erased comparator from a specific comparator instance. + * + * @param sort The specific comparator to wrap + */ + public constructor(sort: SortComparator) : this(sort::compare) + + /** + * Compares two model instances using the wrapped comparator. + * + * @param lhs The left-hand side model instance + * @param rhs The right-hand side model instance + * @param direction The direction of the sort + * @return A comparison result indicating the relative ordering + */ + public fun compare(lhs: T?, rhs: T?, direction: SortDirection): Int { + return this.compare.invoke(lhs, rhs, direction) + } +} + +/** + * Extension function to sort a list of models using a list of sort configurations. + * + * @param T The type of elements in the list. + * @param sort A list of sort configurations to apply to the list. + */ +@StreamInternalApi +public fun List.sortedWith(sort: List>): List = + sortedWith(CompositeComparator(sort)) + +/** + * A composite comparator that combines multiple sort comparators. This class allows for sorting + * based on multiple criteria, where each comparator is applied in sequence. + * + * This implementation mirrors the Swift Array.sorted(using:) extension behavior: + * - Iterates through each sort comparator in order + * - Returns the first non-equal comparison result + * - If all comparators return equal (0), returns 0 to maintain stable sort order + * + * @param T The type of elements to be compared. + * @param comparators The list of comparators to be combined. + */ +@StreamInternalApi +public class CompositeComparator(private val comparators: List>) : Comparator { + + override fun compare(o1: T, o2: T): Int { + for (comparator in comparators) { + val result = comparator.compare(o1, o2) + when (result) { + 0 -> continue // Equal, move to the next comparator + else -> return result // Return the first non-equal comparison result + } + } + return 0 // All comparators returned equal, maintain original order + } +} diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortDirection.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortDirection.kt new file mode 100644 index 0000000..c31e4c6 --- /dev/null +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortDirection.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-core-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android.core.api.filter + +/** + * The direction of a sort operation. This enum defines whether a sort should be performed in + * ascending (forward) or descending (reverse) order. The raw values correspond to the values + * expected by the remote API. + */ +public enum class SortDirection(public val value: Int) { + /** Sort in ascending order (A to Z, 1 to 9, etc.). */ + FORWARD(1), + + /** Sort in descending order (Z to A, 9 to 1, etc.). */ + REVERSE(-1), +} diff --git a/stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortField.kt b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortField.kt new file mode 100644 index 0000000..5c2c002 --- /dev/null +++ b/stream-android-core/src/main/java/io/getstream/android/core/api/filter/SortField.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-core-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android.core.api.filter + +import io.getstream.android.core.annotations.StreamInternalApi +import io.getstream.android.core.annotations.StreamPublishedApi +import io.getstream.android.core.internal.filter.SortFieldImpl + +/** + * A protocol that defines a sortable field for a specific model type. + * + * This interface provides the foundation for creating sortable fields that can be used both for + * local sorting and remote API requests. It includes a comparator for local sorting operations and + * a remote string identifier for API communication. + */ +@StreamPublishedApi +public interface SortField { + /** A comparator that can be used for local sorting operations. */ + public val comparator: AnySortComparator + + /** The string identifier used when sending sort parameters to the remote API. */ + public val remote: String + + public companion object { + /** + * Creates a new sort field with the specified remote identifier and local value extractor. + * + * @param remote The string identifier used for remote API requests + * @param localValue A function that extracts the comparable value from a model instance + */ + @StreamInternalApi + public fun > create( + remote: String, + localValue: (T) -> V, + ): SortField = SortFieldImpl(remote, localValue) + } +} diff --git a/stream-android-core/src/main/java/io/getstream/android/core/internal/filter/FilterOperator.kt b/stream-android-core/src/main/java/io/getstream/android/core/internal/filter/FilterOperator.kt new file mode 100644 index 0000000..fd1e0357 --- /dev/null +++ b/stream-android-core/src/main/java/io/getstream/android/core/internal/filter/FilterOperator.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-core-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android.core.internal.filter + +/** Represents operators that can be used for filtering. */ +internal enum class FilterOperator( + /** The name of this operator as expected by the Stream API. */ + val remote: String +) { + /** + * Matches values that are equal to a specified value or matches all of the values in an array. + */ + EQUAL("\$eq"), + + /** Matches values that are greater than a specified value. */ + GREATER("\$gt"), + + /** Matches values that are greater than or equal to a specified value. */ + GREATER_OR_EQUAL("\$gte"), + + /** Matches values that are less than a specified value. */ + LESS("\$lt"), + + /** Matches values that are less than or equal to a specified value. */ + LESS_OR_EQUAL("\$lte"), + + /** Matches any of the values specified in an array. */ + IN("\$in"), + + /** Matches values by performing text search with the specified value. */ + QUERY("\$q"), + + /** Matches values with the specified text. */ + AUTOCOMPLETE("\$autocomplete"), + + /** Matches values that exist/don't exist based on the specified boolean value. */ + EXISTS("\$exists"), + + /** Matches all the values specified in an array. */ + AND("\$and"), + + /** Matches at least one of the values specified in an array. */ + OR("\$or"), + + /** Matches if the key array contains the given value. */ + CONTAINS("\$contains"), + + /** Matches if the value contains JSON with the given path. */ + PATH_EXISTS("\$path_exists"), +} diff --git a/stream-android-core/src/main/java/io/getstream/android/core/internal/filter/SortFieldImpl.kt b/stream-android-core/src/main/java/io/getstream/android/core/internal/filter/SortFieldImpl.kt new file mode 100644 index 0000000..276a0ac --- /dev/null +++ b/stream-android-core/src/main/java/io/getstream/android/core/internal/filter/SortFieldImpl.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-core-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android.core.internal.filter + +import io.getstream.android.core.api.filter.AnySortComparator +import io.getstream.android.core.api.filter.SortComparator +import io.getstream.android.core.api.filter.SortField + +/** Private implementation of SortField */ +internal class SortFieldImpl>( + override val remote: String, + private val localValue: (T) -> V, +) : SortField { + + override val comparator: AnySortComparator = SortComparator(localValue).toAny() +} diff --git a/stream-android-core/src/test/java/io/getstream/android/core/api/filter/FilterToRequestTest.kt b/stream-android-core/src/test/java/io/getstream/android/core/api/filter/FilterToRequestTest.kt new file mode 100644 index 0000000..1cf693d --- /dev/null +++ b/stream-android-core/src/test/java/io/getstream/android/core/api/filter/FilterToRequestTest.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-core-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android.core.api.filter + +import junit.framework.TestCase +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +internal class FilterToRequestTest( + private val filter: Filter<*>, + private val expectedRequest: Map, + private val testName: String, +) { + + @Test + fun `toRequest should convert typed filter to correct request map`() { + val result = filter.toRequest() + TestCase.assertEquals("Test case: $testName", expectedRequest, result) + } + + companion object { + private data class TestField(override val remote: String) : FilterField + + private val idField = TestField("id") + private val createdAtField = TestField("created_at") + private val textField = TestField("text") + private val filterTagsField = TestField("filter_tags") + private val searchDataField = TestField("search_data") + + @JvmStatic + @Parameterized.Parameters(name = "{2}") + fun data(): Collection> = + listOf( + arrayOf( + idField.equal("activity-123"), + mapOf("id" to mapOf("\$eq" to "activity-123")), + "Field equals value", + ), + arrayOf( + createdAtField.greater(1234567890), + mapOf("created_at" to mapOf("\$gt" to 1234567890)), + "Field greater than value", + ), + arrayOf( + createdAtField.greaterOrEqual(1000000000), + mapOf("created_at" to mapOf("\$gte" to 1000000000)), + "Field greater than or equal to value", + ), + arrayOf( + createdAtField.less(9999999999), + mapOf("created_at" to mapOf("\$lt" to 9999999999)), + "Field less than value", + ), + arrayOf( + createdAtField.lessOrEqual(8888888888), + mapOf("created_at" to mapOf("\$lte" to 8888888888)), + "Field less than or equal to value", + ), + arrayOf( + idField.`in`("id1", "id2", "id3"), + mapOf("id" to mapOf("\$in" to setOf("id1", "id2", "id3"))), + "Field in collection", + ), + arrayOf( + textField.query("search term"), + mapOf("text" to mapOf("\$q" to "search term")), + "Field query search", + ), + arrayOf( + textField.autocomplete("auto prefix"), + mapOf("text" to mapOf("\$autocomplete" to "auto prefix")), + "Field autocomplete search", + ), + arrayOf(idField.exists(), mapOf("id" to mapOf("\$exists" to true)), "Field exists"), + arrayOf( + idField.doesNotExist(), + mapOf("id" to mapOf("\$exists" to false)), + "Field does not exist", + ), + arrayOf( + filterTagsField.contains("tag1"), + mapOf("filter_tags" to mapOf("\$contains" to "tag1")), + "Field contains value", + ), + arrayOf( + searchDataField.pathExists("user.profile"), + mapOf("search_data" to mapOf("\$path_exists" to "user.profile")), + "Field path exists", + ), + arrayOf( + Filters.and(idField.equal("test1"), createdAtField.greater(1234567890)), + mapOf( + "\$and" to + listOf( + mapOf("id" to mapOf("\$eq" to "test1")), + mapOf("created_at" to mapOf("\$gt" to 1234567890)), + ) + ), + "AND filter combination", + ), + arrayOf( + Filters.or(textField.equal("content1"), textField.equal("content2")), + mapOf( + "\$or" to + listOf( + mapOf("text" to mapOf("\$eq" to "content1")), + mapOf("text" to mapOf("\$eq" to "content2")), + ) + ), + "OR filter combination", + ), + arrayOf( + Filters.and( + idField.equal("main-id"), + Filters.or(textField.equal("option1"), textField.equal("option2")), + ), + mapOf( + "\$and" to + listOf( + mapOf("id" to mapOf("\$eq" to "main-id")), + mapOf( + "\$or" to + listOf( + mapOf("text" to mapOf("\$eq" to "option1")), + mapOf("text" to mapOf("\$eq" to "option2")), + ) + ), + ) + ), + "Nested AND containing OR filters", + ), + ) + } +}