diff --git a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/exception/AlgoliaRuntimeException.kt b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/exception/AlgoliaRuntimeException.kt index 96738959ab..f2b72dcaab 100644 --- a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/exception/AlgoliaRuntimeException.kt +++ b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/exception/AlgoliaRuntimeException.kt @@ -54,3 +54,12 @@ public class AlgoliaRetryException( public class AlgoliaWaitException( message: String? = null, ) : AlgoliaRuntimeException(message) + +/** + * Exception thrown when an error occurs during an iterable helper execution. + * + * @param message the detail message + */ +public class AlgoliaIterableException( + message: String? = null, +) : AlgoliaRuntimeException(message) diff --git a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/extensions/SearchClient.kt b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/extensions/SearchClient.kt index da143eb1dd..7617a93840 100644 --- a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/extensions/SearchClient.kt +++ b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/extensions/SearchClient.kt @@ -2,6 +2,7 @@ package com.algolia.client.extensions import com.algolia.client.api.SearchClient import com.algolia.client.exception.AlgoliaApiException +import com.algolia.client.extensions.internal.* import com.algolia.client.extensions.internal.DisjunctiveFaceting import com.algolia.client.extensions.internal.buildRestrictionString import com.algolia.client.extensions.internal.encodeKeySHA256 @@ -583,3 +584,106 @@ public suspend fun SearchClient.searchDisjunctiveFaceting( val responses = searchForHits(queries, requestOptions = requestOptions) return helper.mergeResponses(responses) } + +/** + * Helper: Returns an iterator on top of the `browse` method. + * + * @param indexName The index in which to perform the request. + * @param params The `browse` parameters. + * @param validate The function to validate the response. Default is to check if the cursor is not null. + * @param aggregator The function to aggregate the response. + * @param requestOptions The requestOptions to send along with the query, they will be merged with + * the transporter requestOptions. (optional) + */ +public suspend fun SearchClient.browseObjects( + indexName: String, + params: BrowseParamsObject, + validate: (BrowseResponse) -> Boolean = { response -> response.cursor == null }, + aggregator: ((BrowseResponse) -> Unit), + requestOptions: RequestOptions? = null, +): BrowseResponse { + return createIterable( + execute = { previousResponse -> + browse( + indexName, + params.copy(hitsPerPage = params.hitsPerPage ?: 1000, cursor = previousResponse?.cursor), + requestOptions + ) + }, + validate = validate, + aggregator = aggregator, + ) +} + +/** + * Helper: Returns an iterator on top of the `browse` method. + * + * @param indexName The index in which to perform the request. + * @param searchRulesParams The search rules request parameters + * @param validate The function to validate the response. Default is to check if the cursor is not null. + * @param requestOptions The requestOptions to send along with the query, they will be merged with + * the transporter requestOptions. (optional) + */ +public suspend fun SearchClient.browseRules( + indexName: String, + searchRulesParams: SearchRulesParams, + validate: ((SearchRulesResponse) -> Boolean)? = null, + aggregator: (SearchRulesResponse) -> Unit, + requestOptions: RequestOptions? = null, +): SearchRulesResponse { + val hitsPerPage = searchRulesParams.hitsPerPage ?: 1000 + + return createIterable( + execute = { previousResponse -> + searchRules( + indexName, + searchRulesParams.copy( + page = if (previousResponse != null) (previousResponse.page + 1) else 0, + hitsPerPage = hitsPerPage + ), + requestOptions + ) + }, + validate = validate ?: { response -> response.hits.count() < hitsPerPage }, + aggregator = aggregator, + ) +} + +/** + * Helper: Returns an iterator on top of the `browse` method. + * + * @param indexName The index in which to perform the request. + * @param searchSynonymsParams The search synonyms request parameters + * @param validate The function to validate the response. Default is to check if the cursor is not null. + * @param requestOptions The requestOptions to send along with the query, they will be merged with + * the transporter requestOptions. (optional) + */ +public suspend fun SearchClient.browseSynonyms( + indexName: String, + searchSynonymsParams: SearchSynonymsParams, + validate: ((SearchSynonymsResponse) -> Boolean)? = null, + aggregator: (SearchSynonymsResponse) -> Unit, + requestOptions: RequestOptions? = null, +): SearchSynonymsResponse { + val hitsPerPage = 1000 + var page = searchSynonymsParams.page ?: 0 + + return createIterable( + execute = { _ -> + try { + searchSynonyms( + indexName, + searchSynonymsParams = searchSynonymsParams.copy( + page = page, + hitsPerPage = hitsPerPage + ), + requestOptions + ) + } finally { + page += 1 + } + }, + validate = validate ?: { response -> response.hits.count() < hitsPerPage }, + aggregator = aggregator, + ) +} diff --git a/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/extensions/internal/Iterable.kt b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/extensions/internal/Iterable.kt new file mode 100644 index 0000000000..0bee079fc6 --- /dev/null +++ b/clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/extensions/internal/Iterable.kt @@ -0,0 +1,41 @@ +package com.algolia.client.extensions.internal + +import com.algolia.client.exception.AlgoliaIterableException +import kotlinx.coroutines.delay +import kotlin.time.Duration + +public data class IterableError( + public val validate: (T) -> Boolean, + public val message: ((T) -> String)? = null +) + +public suspend fun createIterable( + execute: suspend (T?) -> T, + validate: (T) -> Boolean, + aggregator: ((T) -> Unit)? = null, + timeout: () -> Duration = { Duration.ZERO }, + error: IterableError? = null +): T { + suspend fun executor(previousResponse: T? = null): T { + val response = execute(previousResponse) + + if (aggregator != null) { + aggregator(response) + } + + if (validate(response)) { + return response + } + + if (error != null && error.validate(response)) { + val message = error.message?.invoke(response) ?: "An error occurred" + throw AlgoliaIterableException(message) + } + + delay(timeout().inWholeMilliseconds) + + return executor(response) + } + + return executor() +}