Skip to content

Commit f12b817

Browse files
committed
Implement support for local filtering
1 parent 1350ef6 commit f12b817

File tree

7 files changed

+523
-55
lines changed

7 files changed

+523
-55
lines changed

stream-android-core/src/main/java/io/getstream/android/core/api/filter/Filter.kt

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,32 +17,69 @@ package io.getstream.android.core.api.filter
1717

1818
import io.getstream.android.core.annotations.StreamInternalApi
1919
import io.getstream.android.core.annotations.StreamPublishedApi
20-
import io.getstream.android.core.internal.filter.FilterOperator
20+
import io.getstream.android.core.internal.filter.BinaryOperator
21+
import io.getstream.android.core.internal.filter.CollectionOperator
22+
import io.getstream.android.core.internal.filter.FilterOperations
2123

2224
/**
2325
* Base interface for filters used in Stream API operations.
2426
*
2527
* Filters are used to specify criteria for querying and retrieving data from Stream services. Each
2628
* filter implementation defines specific matching logic for different comparison operations.
2729
*/
28-
@StreamPublishedApi public sealed interface Filter<F : FilterField>
30+
@StreamPublishedApi public sealed interface Filter<M, F : FilterField<M>>
2931

30-
internal data class BinaryOperationFilter<F : FilterField, V : Any>(
31-
val operator: FilterOperator,
32+
internal data class BinaryOperationFilter<M, F : FilterField<M>>(
33+
val operator: BinaryOperator,
3234
val field: F,
33-
val value: V,
34-
) : Filter<F>
35+
val value: Any,
36+
) : Filter<M, F>
3537

36-
internal data class CollectionOperationFilter<F : FilterField>(
37-
internal val operator: FilterOperator,
38-
val filters: Set<Filter<F>>,
39-
) : Filter<F>
38+
internal data class CollectionOperationFilter<M, F : FilterField<M>>(
39+
internal val operator: CollectionOperator,
40+
val filters: Set<Filter<M, F>>,
41+
) : Filter<M, F>
4042

4143
/** Converts a [Filter] instance to a request map suitable for API queries. */
4244
@StreamInternalApi
43-
public fun Filter<*>.toRequest(): Map<String, Any> =
45+
public fun Filter<*, *>.toRequest(): Map<String, Any> =
4446
when (this) {
4547
is BinaryOperationFilter<*, *> -> mapOf(field.remote to mapOf(operator.remote to value))
46-
is CollectionOperationFilter<*> ->
47-
mapOf(operator.remote to filters.map(Filter<*>::toRequest))
48+
is CollectionOperationFilter<*, *> ->
49+
mapOf(operator.remote to filters.map(Filter<*, *>::toRequest))
50+
}
51+
52+
/** Checks if this filter matches the given item. */
53+
@StreamInternalApi
54+
public infix fun <M, F : FilterField<M>> Filter<M, F>.matches(item: M): Boolean =
55+
when (this) {
56+
is BinaryOperationFilter<M, F> -> {
57+
val fieldValue = field.localValue(item)
58+
val filterValue = value
59+
val notNull = fieldValue != null
60+
61+
with(FilterOperations) {
62+
when (operator) {
63+
BinaryOperator.EQUAL -> notNull && fieldValue == filterValue
64+
BinaryOperator.GREATER -> notNull && fieldValue greater filterValue
65+
BinaryOperator.LESS -> notNull && fieldValue less filterValue
66+
BinaryOperator.GREATER_OR_EQUAL ->
67+
notNull && fieldValue greaterOrEqual filterValue
68+
BinaryOperator.LESS_OR_EQUAL -> notNull && fieldValue lessOrEqual filterValue
69+
BinaryOperator.IN -> notNull && fieldValue `in` filterValue
70+
BinaryOperator.QUERY -> notNull && search(filterValue, where = fieldValue)
71+
BinaryOperator.AUTOCOMPLETE -> notNull && fieldValue autocompletes filterValue
72+
BinaryOperator.EXISTS -> fieldValue exists filterValue
73+
BinaryOperator.CONTAINS -> notNull && fieldValue contains filterValue
74+
BinaryOperator.PATH_EXISTS -> notNull && fieldValue containsPath filterValue
75+
}
76+
}
77+
}
78+
79+
is CollectionOperationFilter<M, F> -> {
80+
when (operator) {
81+
CollectionOperator.AND -> filters.all { it.matches(item) }
82+
CollectionOperator.OR -> filters.any { it.matches(item) }
83+
}
84+
}
4885
}

stream-android-core/src/main/java/io/getstream/android/core/api/filter/FilterField.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ package io.getstream.android.core.api.filter
1919
* Interface representing a field that can be used in filters for querying data from the Stream API.
2020
*
2121
* Implementations of this interface should provide [remote], which is the name of the field as
22-
* expected by the Stream API.
22+
* expected by the Stream API, and [localValue], a function to extract the field's value from a
23+
* model instance.
2324
*/
24-
public interface FilterField {
25+
public interface FilterField<M> {
2526
/** The name of this field as expected by the Stream API. */
2627
public val remote: String
28+
29+
/** Function to extract the field's value from a model instance. */
30+
public val localValue: (M) -> Any?
2731
}

stream-android-core/src/main/java/io/getstream/android/core/api/filter/Filters.kt

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
*/
1616
package io.getstream.android.core.api.filter
1717

18-
import io.getstream.android.core.internal.filter.FilterOperator
18+
import io.getstream.android.core.internal.filter.BinaryOperator
19+
import io.getstream.android.core.internal.filter.CollectionOperator
1920

2021
/** Utility class for building filters. */
2122
public object Filters {
@@ -25,17 +26,17 @@ public object Filters {
2526
* @param filters The filters to combine.
2627
* @return A filter that matches when all provided filters match.
2728
*/
28-
public fun <F : FilterField> and(vararg filters: Filter<F>): Filter<F> =
29-
CollectionOperationFilter(FilterOperator.AND, filters.toSet())
29+
public fun <M, F : FilterField<M>> and(vararg filters: Filter<M, F>): Filter<M, F> =
30+
CollectionOperationFilter(CollectionOperator.AND, filters.toSet())
3031

3132
/**
3233
* Creates a filter that combines multiple filters with a logical OR operation.
3334
*
3435
* @param filters The filters to combine.
3536
* @return A filter that matches when any of the specified filters match.
3637
*/
37-
public fun <F : FilterField> or(vararg filters: Filter<F>): Filter<F> =
38-
CollectionOperationFilter(FilterOperator.OR, filters.toSet())
38+
public fun <M, F : FilterField<M>> or(vararg filters: Filter<M, F>): Filter<M, F> =
39+
CollectionOperationFilter(CollectionOperator.OR, filters.toSet())
3940
}
4041

4142
/**
@@ -44,111 +45,111 @@ public object Filters {
4445
* @param value The value to check equality against.
4546
* @return A filter that matches when this field equals the specified value.
4647
*/
47-
public fun <F : FilterField> F.equal(value: Any): Filter<F> =
48-
BinaryOperationFilter(FilterOperator.EQUAL, this, value)
48+
public fun <M, F : FilterField<M>> F.equal(value: Any): Filter<M, F> =
49+
BinaryOperationFilter(BinaryOperator.EQUAL, this, value)
4950

5051
/**
5152
* Creates a filter that checks if this field is greater than a specific value.
5253
*
5354
* @param value The value to check against.
5455
* @return A filter that matches when this field is greater than the specified value.
5556
*/
56-
public fun <F : FilterField> F.greater(value: Any): Filter<F> =
57-
BinaryOperationFilter(FilterOperator.GREATER, this, value)
57+
public fun <M, F : FilterField<M>> F.greater(value: Any): Filter<M, F> =
58+
BinaryOperationFilter(BinaryOperator.GREATER, this, value)
5859

5960
/**
6061
* Creates a filter that checks if this field is greater than or equal to a specific value.
6162
*
6263
* @param value The value to check against.
6364
* @return A filter that matches when this field is greater than or equal to the specified value.
6465
*/
65-
public fun <F : FilterField> F.greaterOrEqual(value: Any): Filter<F> =
66-
BinaryOperationFilter(FilterOperator.GREATER_OR_EQUAL, this, value)
66+
public fun <M, F : FilterField<M>> F.greaterOrEqual(value: Any): Filter<M, F> =
67+
BinaryOperationFilter(BinaryOperator.GREATER_OR_EQUAL, this, value)
6768

6869
/**
6970
* Creates a filter that checks if this field is less than a specific value.
7071
*
7172
* @param value The value to check against.
7273
* @return A filter that matches when this field is less than the specified value.
7374
*/
74-
public fun <F : FilterField> F.less(value: Any): Filter<F> =
75-
BinaryOperationFilter(FilterOperator.LESS, this, value)
75+
public fun <M, F : FilterField<M>> F.less(value: Any): Filter<M, F> =
76+
BinaryOperationFilter(BinaryOperator.LESS, this, value)
7677

7778
/**
7879
* Creates a filter that checks if this field is less than or equal to a specific value.
7980
*
8081
* @param value The value to check against.
8182
* @return A filter that matches when this field is less than or equal to the specified value.
8283
*/
83-
public fun <F : FilterField> F.lessOrEqual(value: Any): Filter<F> =
84-
BinaryOperationFilter(FilterOperator.LESS_OR_EQUAL, this, value)
84+
public fun <M, F : FilterField<M>> F.lessOrEqual(value: Any): Filter<M, F> =
85+
BinaryOperationFilter(BinaryOperator.LESS_OR_EQUAL, this, value)
8586

8687
/**
8788
* Creates a filter that checks if this field's value is in a specific list of values.
8889
*
8990
* @param values The list of values to check against.
9091
* @return A filter that matches when this field's value is in the specified list.
9192
*/
92-
public fun <F : FilterField> F.`in`(values: List<Any>): Filter<F> =
93-
BinaryOperationFilter(FilterOperator.IN, this, values.toSet())
93+
public fun <M, F : FilterField<M>> F.`in`(values: List<Any>): Filter<M, F> =
94+
BinaryOperationFilter(BinaryOperator.IN, this, values.toSet())
9495

9596
/**
9697
* Creates a filter that checks if this field's value is in a specific set of values.
9798
*
9899
* @param values The values to check against.
99100
* @return A filter that matches when this field's value is in the specified values.
100101
*/
101-
public fun <F : FilterField> F.`in`(vararg values: Any): Filter<F> =
102-
BinaryOperationFilter(FilterOperator.IN, this, values.toSet())
102+
public fun <M, F : FilterField<M>> F.`in`(vararg values: Any): Filter<M, F> =
103+
BinaryOperationFilter(BinaryOperator.IN, this, values.toSet())
103104

104105
/**
105106
* Creates a filter that performs a full-text query on this field.
106107
*
107108
* @param value The query string to search for.
108109
* @return A filter that matches based on the full-text query.
109110
*/
110-
public fun <F : FilterField> F.query(value: String): Filter<F> =
111-
BinaryOperationFilter(FilterOperator.QUERY, this, value)
111+
public fun <M, F : FilterField<M>> F.query(value: String): Filter<M, F> =
112+
BinaryOperationFilter(BinaryOperator.QUERY, this, value)
112113

113114
/**
114115
* Creates a filter that performs autocomplete matching on this field.
115116
*
116117
* @param value The string to autocomplete against.
117118
* @return A filter that matches based on autocomplete functionality.
118119
*/
119-
public fun <F : FilterField> F.autocomplete(value: String): Filter<F> =
120-
BinaryOperationFilter(FilterOperator.AUTOCOMPLETE, this, value)
120+
public fun <M, F : FilterField<M>> F.autocomplete(value: String): Filter<M, F> =
121+
BinaryOperationFilter(BinaryOperator.AUTOCOMPLETE, this, value)
121122

122123
/**
123124
* Creates a filter that checks if this field exists.
124125
*
125126
* @return A filter that matches when this field exists.
126127
*/
127-
public fun <F : FilterField> F.exists(): Filter<F> =
128-
BinaryOperationFilter(FilterOperator.EXISTS, this, true)
128+
public fun <M, F : FilterField<M>> F.exists(): Filter<M, F> =
129+
BinaryOperationFilter(BinaryOperator.EXISTS, this, true)
129130

130131
/**
131132
* Creates a filter that checks if this field does not exist.
132133
*
133134
* @return A filter that matches when this field does not exist.
134135
*/
135-
public fun <F : FilterField> F.doesNotExist(): Filter<F> =
136-
BinaryOperationFilter(FilterOperator.EXISTS, this, false)
136+
public fun <M, F : FilterField<M>> F.doesNotExist(): Filter<M, F> =
137+
BinaryOperationFilter(BinaryOperator.EXISTS, this, false)
137138

138139
/**
139140
* Creates a filter that checks if this field contains a specific value.
140141
*
141142
* @param value The value to check for within this field.
142143
* @return A filter that matches when this field contains the specified value.
143144
*/
144-
public fun <F : FilterField> F.contains(value: Any): Filter<F> =
145-
BinaryOperationFilter(FilterOperator.CONTAINS, this, value)
145+
public fun <M, F : FilterField<M>> F.contains(value: Any): Filter<M, F> =
146+
BinaryOperationFilter(BinaryOperator.CONTAINS, this, value)
146147

147148
/**
148149
* Creates a filter that checks if a specific path exists within this field.
149150
*
150151
* @param value The path to check for existence.
151152
* @return A filter that matches when the specified path exists in this field.
152153
*/
153-
public fun <F : FilterField> F.pathExists(value: String): Filter<F> =
154-
BinaryOperationFilter(FilterOperator.PATH_EXISTS, this, value)
154+
public fun <M, F : FilterField<M>> F.pathExists(value: String): Filter<M, F> =
155+
BinaryOperationFilter(BinaryOperator.PATH_EXISTS, this, value)
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright (c) 2014-2025 Stream.io Inc. All rights reserved.
3+
*
4+
* Licensed under the Stream License;
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://github.com/GetStream/stream-core-android/blob/main/LICENSE
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.getstream.android.core.internal.filter
17+
18+
internal object FilterOperations {
19+
infix fun Any.greater(that: Any) = anyCompare(this, that)?.let { it > 0 } == true
20+
21+
infix fun Any.greaterOrEqual(that: Any) = anyCompare(this, that)?.let { it >= 0 } == true
22+
23+
infix fun Any.less(that: Any) = anyCompare(this, that)?.let { it < 0 } == true
24+
25+
infix fun Any.lessOrEqual(that: Any) = anyCompare(this, that)?.let { it <= 0 } == true
26+
27+
private fun anyCompare(a: Any, b: Any): Int? {
28+
if (a !is Comparable<*>) {
29+
return null
30+
}
31+
32+
return try {
33+
@Suppress("UNCHECKED_CAST") (a as Comparable<Any>).compareTo(b)
34+
} catch (_: ClassCastException) {
35+
// The types were not compatible for comparison
36+
null
37+
}
38+
}
39+
40+
infix fun Any?.exists(that: Any): Boolean = (that is Boolean) && (this != null) == that
41+
42+
infix fun Any.`in`(that: Any): Boolean =
43+
when (that) {
44+
is Array<*> -> this in that
45+
is Iterable<*> -> this in that
46+
else -> false
47+
}
48+
49+
infix fun Any.contains(that: Any): Boolean =
50+
when {
51+
that `in` this -> true
52+
53+
this is Map<*, *> && that is Map<*, *> -> {
54+
// Partial match: check if all entries in 'that' are present in 'this'
55+
that.all { (thatKey, thatValue) ->
56+
val thisValue = this[thatKey]
57+
58+
thisValue == thatValue ||
59+
thisValue != null && thatValue != null && thisValue contains thatValue
60+
}
61+
}
62+
63+
else -> false
64+
}
65+
66+
infix fun Any.autocompletes(that: Any): Boolean {
67+
if (this !is String || that !is String || that.isEmpty()) {
68+
return false
69+
}
70+
71+
// Split the text into words using whitespace and punctuation as delimiters
72+
return this.split(Regex("[\\s\\p{Punct}]+")).any { word -> word.startsWith(that, true) }
73+
}
74+
75+
fun search(what: Any, where: Any): Boolean =
76+
what is String &&
77+
where is String &&
78+
what.isNotEmpty() &&
79+
where.contains(what, ignoreCase = true)
80+
81+
infix fun Any.containsPath(that: Any): Boolean {
82+
if (this !is Map<*, *> || that !is String) return false
83+
84+
val pathParts = that.split(".")
85+
var current: Any? = this
86+
87+
for (part in pathParts) {
88+
when {
89+
current !is Map<*, *> -> return false
90+
part !in current -> return false
91+
else -> current = current[part]
92+
}
93+
}
94+
95+
return true
96+
}
97+
}

0 commit comments

Comments
 (0)