Skip to content

Commit 8aab51b

Browse files
committed
Add sort and type-safe filters
1 parent 523e558 commit 8aab51b

File tree

10 files changed

+700
-0
lines changed

10 files changed

+700
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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.api.filter
17+
18+
import io.getstream.android.core.annotations.StreamInternalApi
19+
import io.getstream.android.core.annotations.StreamPublishedApi
20+
import io.getstream.android.core.internal.filter.FilterOperator
21+
22+
/**
23+
* Base interface for filters used in Stream API operations.
24+
*
25+
* Filters are used to specify criteria for querying and retrieving data from Stream services. Each
26+
* filter implementation defines specific matching logic for different comparison operations.
27+
*/
28+
@StreamPublishedApi
29+
public sealed interface Filter<F : FilterField>
30+
31+
internal data class BinaryOperationFilter<F : FilterField, V : Any>(
32+
val operator: FilterOperator,
33+
val field: F,
34+
val value: V,
35+
) : Filter<F>
36+
37+
internal data class CollectionOperationFilter<F : FilterField>(
38+
internal val operator: FilterOperator,
39+
val filters: Set<Filter<F>>,
40+
) : Filter<F>
41+
42+
/** Converts a [Filter] instance to a request map suitable for API queries. */
43+
@StreamInternalApi
44+
public fun Filter<*>.toRequest(): Map<String, Any> =
45+
when (this) {
46+
is BinaryOperationFilter<*, *> -> mapOf(field.remote to mapOf(operator.remote to value))
47+
is CollectionOperationFilter<*> ->
48+
mapOf(operator.remote to filters.map(Filter<*>::toRequest))
49+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.api.filter
17+
18+
/**
19+
* Interface representing a field that can be used in filters for querying data from the Stream API.
20+
*
21+
* Implementations of this interface should provide [remote], which is the name of the field as
22+
* expected by the Stream API.
23+
*/
24+
public interface FilterField {
25+
/** The name of this field as expected by the Stream API. */
26+
public val remote: String
27+
}
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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.api.filter
17+
18+
import io.getstream.android.core.internal.filter.FilterOperator
19+
20+
/** Utility class for building filters. */
21+
public object Filters {
22+
/**
23+
* Creates a filter that combines multiple filters with a logical AND operation.
24+
*
25+
* @param filters The filters to combine.
26+
* @return A filter that matches when all provided filters match.
27+
*/
28+
public fun <F : FilterField> and(vararg filters: Filter<F>): Filter<F> =
29+
CollectionOperationFilter(FilterOperator.AND, filters.toSet())
30+
31+
/**
32+
* Creates a filter that combines multiple filters with a logical OR operation.
33+
*
34+
* @param filters The filters to combine.
35+
* @return A filter that matches when any of the specified filters match.
36+
*/
37+
public fun <F : FilterField> or(vararg filters: Filter<F>): Filter<F> =
38+
CollectionOperationFilter(FilterOperator.OR, filters.toSet())
39+
}
40+
41+
/**
42+
* Creates a filter that checks if this field equals a specific value.
43+
*
44+
* @param value The value to check equality against.
45+
* @return A filter that matches when this field equals the specified value.
46+
*/
47+
public fun <F : FilterField> F.equal(value: Any): Filter<F> =
48+
BinaryOperationFilter(FilterOperator.EQUAL, this, value)
49+
50+
/**
51+
* Creates a filter that checks if this field is greater than a specific value.
52+
*
53+
* @param value The value to check against.
54+
* @return A filter that matches when this field is greater than the specified value.
55+
*/
56+
public fun <F : FilterField> F.greater(value: Any): Filter<F> =
57+
BinaryOperationFilter(FilterOperator.GREATER, this, value)
58+
59+
/**
60+
* Creates a filter that checks if this field is greater than or equal to a specific value.
61+
*
62+
* @param value The value to check against.
63+
* @return A filter that matches when this field is greater than or equal to the specified value.
64+
*/
65+
public fun <F : FilterField> F.greaterOrEqual(value: Any): Filter<F> =
66+
BinaryOperationFilter(FilterOperator.GREATER_OR_EQUAL, this, value)
67+
68+
/**
69+
* Creates a filter that checks if this field is less than a specific value.
70+
*
71+
* @param value The value to check against.
72+
* @return A filter that matches when this field is less than the specified value.
73+
*/
74+
public fun <F : FilterField> F.less(value: Any): Filter<F> =
75+
BinaryOperationFilter(FilterOperator.LESS, this, value)
76+
77+
/**
78+
* Creates a filter that checks if this field is less than or equal to a specific value.
79+
*
80+
* @param value The value to check against.
81+
* @return A filter that matches when this field is less than or equal to the specified value.
82+
*/
83+
public fun <F : FilterField> F.lessOrEqual(value: Any): Filter<F> =
84+
BinaryOperationFilter(FilterOperator.LESS_OR_EQUAL, this, value)
85+
86+
/**
87+
* Creates a filter that checks if this field's value is in a specific list of values.
88+
*
89+
* @param values The list of values to check against.
90+
* @return A filter that matches when this field's value is in the specified list.
91+
*/
92+
public fun <F : FilterField> F.`in`(values: List<Any>): Filter<F> =
93+
BinaryOperationFilter(FilterOperator.IN, this, values.toSet())
94+
95+
/**
96+
* Creates a filter that checks if this field's value is in a specific set of values.
97+
*
98+
* @param values The values to check against.
99+
* @return A filter that matches when this field's value is in the specified values.
100+
*/
101+
public fun <F : FilterField> F.`in`(vararg values: Any): Filter<F> =
102+
BinaryOperationFilter(FilterOperator.IN, this, values.toSet())
103+
104+
/**
105+
* Creates a filter that performs a full-text query on this field.
106+
*
107+
* @param value The query string to search for.
108+
* @return A filter that matches based on the full-text query.
109+
*/
110+
public fun <F : FilterField> F.query(value: String): Filter<F> =
111+
BinaryOperationFilter(FilterOperator.QUERY, this, value)
112+
113+
/**
114+
* Creates a filter that performs autocomplete matching on this field.
115+
*
116+
* @param value The string to autocomplete against.
117+
* @return A filter that matches based on autocomplete functionality.
118+
*/
119+
public fun <F : FilterField> F.autocomplete(value: String): Filter<F> =
120+
BinaryOperationFilter(FilterOperator.AUTOCOMPLETE, this, value)
121+
122+
/**
123+
* Creates a filter that checks if this field exists.
124+
*
125+
* @return A filter that matches when this field exists.
126+
*/
127+
public fun <F : FilterField> F.exists(): Filter<F> =
128+
BinaryOperationFilter(FilterOperator.EXISTS, this, true)
129+
130+
/**
131+
* Creates a filter that checks if this field does not exist.
132+
*
133+
* @return A filter that matches when this field does not exist.
134+
*/
135+
public fun <F : FilterField> F.doesNotExist(): Filter<F> =
136+
BinaryOperationFilter(FilterOperator.EXISTS, this, false)
137+
138+
/**
139+
* Creates a filter that checks if this field contains a specific value.
140+
*
141+
* @param value The value to check for within this field.
142+
* @return A filter that matches when this field contains the specified value.
143+
*/
144+
public fun <F : FilterField> F.contains(value: Any): Filter<F> =
145+
BinaryOperationFilter(FilterOperator.CONTAINS, this, value)
146+
147+
/**
148+
* Creates a filter that checks if a specific path exists within this field.
149+
*
150+
* @param value The path to check for existence.
151+
* @return A filter that matches when the specified path exists in this field.
152+
*/
153+
public fun <F : FilterField> F.pathExists(value: String): Filter<F> =
154+
BinaryOperationFilter(FilterOperator.PATH_EXISTS, this, value)
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
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.api.filter
17+
18+
/**
19+
* A sort configuration that combines a sort field with a direction.
20+
*
21+
* This class represents a complete sort specification that can be applied to collections of the
22+
* associated model type. It provides both local sorting capabilities and the ability to generate
23+
* remote API request parameters.
24+
*/
25+
public open class Sort<T>(public val field: SortField<T>, public val direction: SortDirection) :
26+
Comparator<T> {
27+
28+
/** Converts this sort configuration to a DTO map for API requests. */
29+
public fun toDto(): Map<String, Any> =
30+
mapOf("field" to field.remote, "direction" to direction.value)
31+
32+
override fun compare(o1: T?, o2: T?): Int {
33+
return field.comparator.compare(o1, o2, direction)
34+
}
35+
}
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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.api.filter
17+
18+
import io.getstream.android.core.annotations.StreamInternalApi
19+
20+
/**
21+
* A comparator that can sort model instances by extracting comparable values.
22+
*
23+
* This class provides the foundation for local sorting operations by wrapping a lambda that
24+
* extracts comparable values from model instances. It handles the comparison logic and direction
25+
* handling internally.
26+
*
27+
* @param T The type of the model instances to be compared.
28+
* @param V The type of the comparable value extracted from the model instances.
29+
* @property value A lambda that extracts a comparable value from a model instance.
30+
*/
31+
public class SortComparator<T, V : Comparable<V>>(public val value: (T) -> V) {
32+
33+
/**
34+
* Compares two model instances using the extracted values and sort direction.
35+
*
36+
* @param lhs The first model instance to compare
37+
* @param rhs The second model instance to compare
38+
* @param direction The direction of the sort
39+
* @return A comparison result indicating the relative ordering
40+
*/
41+
public fun compare(lhs: T?, rhs: T?, direction: SortDirection): Int {
42+
val value1 = lhs?.let(value)
43+
val value2 = rhs?.let(value)
44+
45+
return when {
46+
value1 == null && value2 == null -> 0
47+
value1 == null -> -direction.value
48+
value2 == null -> direction.value
49+
else -> value1.compareTo(value2) * direction.value
50+
}
51+
}
52+
53+
/**
54+
* Converts this comparator to a type-erased version.
55+
*
56+
* @return An AnySortComparator that wraps this comparator
57+
*/
58+
public fun toAny(): AnySortComparator<T> {
59+
return AnySortComparator(this)
60+
}
61+
}
62+
63+
/**
64+
* A type-erased wrapper for sort comparators that can work with any model type.
65+
*
66+
* This class provides a way to store and use sort comparators without knowing their specific
67+
* generic type parameters. It's useful for creating collections of different sort configurations
68+
* that can all work with the same model type.
69+
*
70+
* Type erased type avoids making SortField generic while keeping the underlying value type intact
71+
* (no runtime type checks while sorting).
72+
*/
73+
public class AnySortComparator<T>(private val compare: (T?, T?, SortDirection) -> Int) {
74+
75+
/**
76+
* Creates a type-erased comparator from a specific comparator instance.
77+
*
78+
* @param sort The specific comparator to wrap
79+
*/
80+
public constructor(sort: SortComparator<T, *>) : this(sort::compare)
81+
82+
/**
83+
* Compares two model instances using the wrapped comparator.
84+
*
85+
* @param lhs The left-hand side model instance
86+
* @param rhs The right-hand side model instance
87+
* @param direction The direction of the sort
88+
* @return A comparison result indicating the relative ordering
89+
*/
90+
public fun compare(lhs: T?, rhs: T?, direction: SortDirection): Int {
91+
return this.compare.invoke(lhs, rhs, direction)
92+
}
93+
}
94+
95+
/**
96+
* Extension function to sort a list of models using a list of sort configurations.
97+
*
98+
* @param T The type of elements in the list.
99+
* @param sort A list of sort configurations to apply to the list.
100+
*/
101+
@StreamInternalApi
102+
public fun <T> List<T>.sortedWith(sort: List<Sort<T>>): List<T> =
103+
sortedWith(CompositeComparator(sort))
104+
105+
/**
106+
* A composite comparator that combines multiple sort comparators. This class allows for sorting
107+
* based on multiple criteria, where each comparator is applied in sequence.
108+
*
109+
* This implementation mirrors the Swift Array.sorted(using:) extension behavior:
110+
* - Iterates through each sort comparator in order
111+
* - Returns the first non-equal comparison result
112+
* - If all comparators return equal (0), returns 0 to maintain stable sort order
113+
*
114+
* @param T The type of elements to be compared.
115+
* @param comparators The list of comparators to be combined.
116+
*/
117+
@StreamInternalApi
118+
public class CompositeComparator<T>(private val comparators: List<Comparator<T>>) : Comparator<T> {
119+
120+
override fun compare(o1: T, o2: T): Int {
121+
for (comparator in comparators) {
122+
val result = comparator.compare(o1, o2)
123+
when (result) {
124+
0 -> continue // Equal, move to the next comparator
125+
else -> return result // Return the first non-equal comparison result
126+
}
127+
}
128+
return 0 // All comparators returned equal, maintain original order
129+
}
130+
}

0 commit comments

Comments
 (0)