Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -17,32 +17,69 @@ 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
import io.getstream.android.core.internal.filter.BinaryOperator
import io.getstream.android.core.internal.filter.CollectionOperator
import io.getstream.android.core.internal.filter.FilterOperations

/**
* 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<F : FilterField>
@StreamPublishedApi public sealed interface Filter<M, F : FilterField<M>>

internal data class BinaryOperationFilter<F : FilterField, V : Any>(
val operator: FilterOperator,
internal data class BinaryOperationFilter<M, F : FilterField<M>>(
val operator: BinaryOperator,
val field: F,
val value: V,
) : Filter<F>
val value: Any,
) : Filter<M, F>

internal data class CollectionOperationFilter<F : FilterField>(
internal val operator: FilterOperator,
val filters: Set<Filter<F>>,
) : Filter<F>
internal data class CollectionOperationFilter<M, F : FilterField<M>>(
internal val operator: CollectionOperator,
val filters: Set<Filter<M, F>>,
) : Filter<M, F>

/** Converts a [Filter] instance to a request map suitable for API queries. */
@StreamInternalApi
public fun Filter<*>.toRequest(): Map<String, Any> =
public fun Filter<*, *>.toRequest(): Map<String, Any> =
when (this) {
is BinaryOperationFilter<*, *> -> mapOf(field.remote to mapOf(operator.remote to value))
is CollectionOperationFilter<*> ->
mapOf(operator.remote to filters.map(Filter<*>::toRequest))
is CollectionOperationFilter<*, *> ->
mapOf(operator.remote to filters.map(Filter<*, *>::toRequest))
}

/** Checks if this filter matches the given item. */
@StreamInternalApi
public infix fun <M, F : FilterField<M>> Filter<M, F>.matches(item: M): Boolean =
when (this) {
is BinaryOperationFilter<M, F> -> {
val fieldValue = field.localValue(item)
val filterValue = value
val notNull = fieldValue != null

with(FilterOperations) {
when (operator) {
BinaryOperator.EQUAL -> notNull && fieldValue == filterValue
BinaryOperator.GREATER -> notNull && fieldValue greater filterValue
BinaryOperator.LESS -> notNull && fieldValue less filterValue
BinaryOperator.GREATER_OR_EQUAL ->
notNull && fieldValue greaterOrEqual filterValue
BinaryOperator.LESS_OR_EQUAL -> notNull && fieldValue lessOrEqual filterValue
BinaryOperator.IN -> notNull && fieldValue `in` filterValue
BinaryOperator.QUERY -> notNull && search(filterValue, where = fieldValue)
BinaryOperator.AUTOCOMPLETE -> notNull && fieldValue autocompletes filterValue
BinaryOperator.EXISTS -> fieldValue exists filterValue
BinaryOperator.CONTAINS -> notNull && fieldValue contains filterValue
BinaryOperator.PATH_EXISTS -> notNull && fieldValue containsPath filterValue
}
}
}

is CollectionOperationFilter<M, F> -> {
when (operator) {
CollectionOperator.AND -> filters.all { it.matches(item) }
CollectionOperator.OR -> filters.any { it.matches(item) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ 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.
* expected by the Stream API, and [localValue], a function to extract the field's value from a
* model instance.
*/
public interface FilterField {
public interface FilterField<M> {
/** The name of this field as expected by the Stream API. */
public val remote: String

/** Function to extract the field's value from a model instance. */
public val localValue: (M) -> Any?
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
*/
package io.getstream.android.core.api.filter
Copy link
Contributor

@VelikovPetar VelikovPetar Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated to this PR: I can see that the io.getstream.android.core.api.filter also contains the Sort logic. Perhaps we should move it to another package sort, or to rename this whole package to something like query, which will hold both filtering and sorting logic (would be good to unify this before the first major release to avoid breaking changes).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember @aleksandar-apostolov not being fully convinced about the query name for filtering. I wanted to find an even more generic name, but couldn't. I asked cursor and it agrees with you :D

Screenshot 2025-09-19 at 16 27 16

It also suggested other stuff, but I don't think they're fitting

Screenshot 2025-09-19 at 16 28 52

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was not convinced because it was called query and only had Filters.kt in it.
But if its 3 on 1 (including Cursor, feel free to rename it)

Here is Codex opinion btw:

The code in io.getstream.android.core.api.filter and io.getstream.android.core.internal.filter is all about the filtering DSL—operators, filter builders, and sort helpers—not about end-to-end “query” construction.

  • Public API surface such as Filters.and, Filters.or, and the extension helpers like FilterField.equal, FilterField.in, etc. are strictly building filter expressions that map onto the server-side filter JSON (Filters.kt:20-154).
  • Those helpers are backed by the internal pieces FilterOperator and SortFieldImpl (FilterOperator.kt:16-63, SortFieldImpl.kt:16-31), which simply translate our filter clauses into the $eq, $in, $and, … operators the API expects.
    A “query” in Stream terminology usually combines multiple concerns (filters, sort, pagination limits, watch flags, etc.), and there are already request DTOs and client methods that represent full queries elsewhere in the SDK. Renaming this package to query would therefore overstate what the code does and blur the separation between the low-level filter DSL and the higher-level query request objects. Keeping it as filter/filters makes it clear these classes are the filter-and-sort building blocks that other query-related components consume.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify I asked Codex "Is query a better package name for the filter package." :)

Copy link
Collaborator Author

@gpunto gpunto Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I took Petar's first suggestion and moved sort code to its own package, so we avoid the query name, since core doesn't know anything about queries now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me!
If @aleksandar-apostolov agrees as well, I would say to go ahead and merge it!


import io.getstream.android.core.internal.filter.FilterOperator
import io.getstream.android.core.annotations.StreamPublishedApi
import io.getstream.android.core.internal.filter.BinaryOperator
import io.getstream.android.core.internal.filter.CollectionOperator

/** Utility class for building filters. */
public object Filters {
Expand All @@ -25,17 +27,19 @@ public object Filters {
* @param filters The filters to combine.
* @return A filter that matches when all provided filters match.
*/
public fun <F : FilterField> and(vararg filters: Filter<F>): Filter<F> =
CollectionOperationFilter(FilterOperator.AND, filters.toSet())
@StreamPublishedApi
public fun <M, F : FilterField<M>> and(vararg filters: Filter<M, F>): Filter<M, F> =
CollectionOperationFilter(CollectionOperator.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 <F : FilterField> or(vararg filters: Filter<F>): Filter<F> =
CollectionOperationFilter(FilterOperator.OR, filters.toSet())
@StreamPublishedApi
public fun <M, F : FilterField<M>> or(vararg filters: Filter<M, F>): Filter<M, F> =
CollectionOperationFilter(CollectionOperator.OR, filters.toSet())
}

/**
Expand All @@ -44,111 +48,124 @@ public object Filters {
* @param value The value to check equality against.
* @return A filter that matches when this field equals the specified value.
*/
public fun <F : FilterField> F.equal(value: Any): Filter<F> =
BinaryOperationFilter(FilterOperator.EQUAL, this, value)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.equal(value: Any): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.greater(value: Any): Filter<F> =
BinaryOperationFilter(FilterOperator.GREATER, this, value)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.greater(value: Any): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.greaterOrEqual(value: Any): Filter<F> =
BinaryOperationFilter(FilterOperator.GREATER_OR_EQUAL, this, value)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.greaterOrEqual(value: Any): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.less(value: Any): Filter<F> =
BinaryOperationFilter(FilterOperator.LESS, this, value)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.less(value: Any): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.lessOrEqual(value: Any): Filter<F> =
BinaryOperationFilter(FilterOperator.LESS_OR_EQUAL, this, value)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.lessOrEqual(value: Any): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.`in`(values: List<Any>): Filter<F> =
BinaryOperationFilter(FilterOperator.IN, this, values.toSet())
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.`in`(values: List<Any>): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.`in`(vararg values: Any): Filter<F> =
BinaryOperationFilter(FilterOperator.IN, this, values.toSet())
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.`in`(vararg values: Any): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.query(value: String): Filter<F> =
BinaryOperationFilter(FilterOperator.QUERY, this, value)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.query(value: String): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.autocomplete(value: String): Filter<F> =
BinaryOperationFilter(FilterOperator.AUTOCOMPLETE, this, value)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.autocomplete(value: String): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.AUTOCOMPLETE, this, value)

/**
* Creates a filter that checks if this field exists.
*
* @return A filter that matches when this field exists.
*/
public fun <F : FilterField> F.exists(): Filter<F> =
BinaryOperationFilter(FilterOperator.EXISTS, this, true)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.exists(): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.doesNotExist(): Filter<F> =
BinaryOperationFilter(FilterOperator.EXISTS, this, false)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.doesNotExist(): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.contains(value: Any): Filter<F> =
BinaryOperationFilter(FilterOperator.CONTAINS, this, value)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.contains(value: Any): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.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 : FilterField> F.pathExists(value: String): Filter<F> =
BinaryOperationFilter(FilterOperator.PATH_EXISTS, this, value)
@StreamPublishedApi
public fun <M, F : FilterField<M>> F.pathExists(value: String): Filter<M, F> =
BinaryOperationFilter(BinaryOperator.PATH_EXISTS, this, value)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.getstream.android.core.api.filter
package io.getstream.android.core.api.sort

/**
* A sort configuration that combines a sort field with a direction.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.getstream.android.core.api.filter
package io.getstream.android.core.api.sort

import io.getstream.android.core.annotations.StreamInternalApi

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.getstream.android.core.api.filter
package io.getstream.android.core.api.sort

/**
* The direction of a sort operation. This enum defines whether a sort should be performed in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.getstream.android.core.api.filter
package io.getstream.android.core.api.sort

import io.getstream.android.core.annotations.StreamInternalApi
import io.getstream.android.core.annotations.StreamPublishedApi
Expand Down
Loading