Skip to content

Commit

Permalink
add fallback body to paginator to handle empty responses
Browse files Browse the repository at this point in the history
  • Loading branch information
anssari1 committed Jan 29, 2025
1 parent 173a5cb commit 24c82e3
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import io.ktor.client.statement.HttpResponse
sealed class BasePaginator<R, T>(
private val client: Client,
firstResponse: Response<T>,
private val fallbackBody: T,
private val getBody: suspend (HttpResponse) -> T
) : Iterator<R> {
private var state: ResponseState<T> = DefaultResponseState(firstResponse)
Expand All @@ -41,7 +42,7 @@ sealed class BasePaginator<R, T>(

protected fun nextResponse(): Response<T> {
val response = state.getNextResponse()
state = ResponseStateFactory.getState(extractLink(response.headers), client, getBody)
state = ResponseStateFactory.getState(extractLink(response.headers), client, fallbackBody, getBody)
return response
}
}
Expand All @@ -56,8 +57,9 @@ sealed class BasePaginator<R, T>(
class Paginator<T>(
client: Client,
firstResponse: Response<T>,
fallbackBody: T,
getBody: suspend (HttpResponse) -> T
) : BasePaginator<T, T>(client, firstResponse, getBody) {
) : BasePaginator<T, T>(client, firstResponse, fallbackBody, getBody) {
/**
* Returns the body of the next response.
*
Expand All @@ -76,8 +78,9 @@ class Paginator<T>(
class ResponsePaginator<T>(
client: Client,
firstResponse: Response<T>,
fallbackBody: T,
getBody: suspend (HttpResponse) -> T
) : BasePaginator<Response<T>, T>(client, firstResponse, getBody) {
) : BasePaginator<Response<T>, T>(client, firstResponse, fallbackBody, getBody) {
/**
* Returns the next response.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package com.expediagroup.sdk.core.model.paging
import com.expediagroup.sdk.core.client.Client
import com.expediagroup.sdk.core.model.Response
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsBytes
import kotlinx.coroutines.runBlocking

internal interface ResponseState<T> {
Expand Down Expand Up @@ -51,6 +52,7 @@ internal class LastResponseState<T> : ResponseState<T> {
internal class FetchLinkState<T>(
private val link: String,
private val client: Client,
private val fallbackBody: T,
private val getBody: suspend (HttpResponse) -> T
) : ResponseState<T> {
override fun getNextResponse(): Response<T> {
Expand All @@ -66,7 +68,9 @@ internal class FetchLinkState<T>(
}

private suspend fun parseBody(response: HttpResponse): T {
return getBody(response)
// response.bodyAsBytes() applies all plugins
// if content-length header is set, response.contentLength could be used instead
return if (response.bodyAsBytes().isEmpty()) fallbackBody else getBody(response)
}
}

Expand All @@ -75,9 +79,10 @@ internal class ResponseStateFactory {
fun <T> getState(
link: String?,
client: Client,
fallbackBody: T,
getBody: suspend (HttpResponse) -> T
): ResponseState<T> {
return link?.let { FetchLinkState(it, client, getBody) } ?: LastResponseState()
return link?.let { FetchLinkState(it, client, fallbackBody, getBody) } ?: LastResponseState()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class PaginatorTest {
fun `test paginator with one response`() {
val firstResponse = Response(200, "first", emptyMap())

val paginator = Paginator(client, firstResponse, getBody)
val paginator = Paginator(client, firstResponse, EMPTY_STRING, getBody)
assertTrue(paginator.hasNext())
assertEquals("first", paginator.next())
assertFalse(paginator.hasNext())
Expand All @@ -54,7 +54,7 @@ class PaginatorTest {
fun `test paginator with multiple responses`() {
val firstResponse = Response(200, "first", mapOf("link" to listOf("<second>; rel=\"next\"")))

val paginator = Paginator(client, firstResponse, getBody)
val paginator = Paginator(client, firstResponse, EMPTY_STRING, getBody)
assertTrue(paginator.hasNext())
assertEquals("first", paginator.next())
assertTrue(paginator.hasNext())
Expand All @@ -66,7 +66,7 @@ class PaginatorTest {
fun `test paginator with multiple responses and total results`() {
val firstResponse = Response(200, "first", mapOf("link" to listOf("<second>; rel=\"next\""), "pagination-total-results" to listOf("2")))

val paginator = Paginator(client, firstResponse, getBody)
val paginator = Paginator(client, firstResponse, EMPTY_STRING, getBody)
assertTrue(paginator.hasNext())
assertEquals("first", paginator.next())
assertTrue(paginator.hasNext())
Expand All @@ -79,7 +79,7 @@ class PaginatorTest {
fun `test paginator as list`() {
val firstResponse = Response(200, "first", mapOf("link" to listOf("<second>; rel=\"next\""), "pagination-total-results" to listOf("2")))

val paginator = Paginator(client, firstResponse, getBody)
val paginator = Paginator(client, firstResponse, EMPTY_STRING, getBody)
val list = paginator.asSequence().toList()
assertEquals(2, list.size)
assertEquals("first", list[0])
Expand All @@ -93,7 +93,7 @@ class PaginatorTest {
fun `test response paginator with one response`() {
val firstResponse = Response(200, "first", emptyMap())

val paginator = ResponsePaginator(client, firstResponse, getBody)
val paginator = ResponsePaginator(client, firstResponse, EMPTY_STRING, getBody)
assertTrue(paginator.hasNext())
assertEquals("first", paginator.next().data)
assertFalse(paginator.hasNext())
Expand All @@ -103,7 +103,7 @@ class PaginatorTest {
fun `test response paginator with multiple responses`() {
val firstResponse = Response(200, "first", mapOf("link" to listOf("<second>; rel=\"next\"")))

val paginator = ResponsePaginator(client, firstResponse, getBody)
val paginator = ResponsePaginator(client, firstResponse, EMPTY_STRING, getBody)
assertTrue(paginator.hasNext())
assertEquals("first", paginator.next().data)
assertTrue(paginator.hasNext())
Expand All @@ -115,7 +115,7 @@ class PaginatorTest {
fun `test response paginator with multiple responses and total results`() {
val firstResponse = Response(200, "first", mapOf("link" to listOf("<second>; rel=\"next\""), "pagination-total-results" to listOf("2")))

val paginator = ResponsePaginator(client, firstResponse, getBody)
val paginator = ResponsePaginator(client, firstResponse, EMPTY_STRING, getBody)
assertTrue(paginator.hasNext())
assertEquals("first", paginator.next().data)
assertTrue(paginator.hasNext())
Expand All @@ -124,25 +124,27 @@ class PaginatorTest {
assertEquals(2, paginator.paginationTotalResults)
}

@Test
fun `should return empty value when next response body is empty`() {
@ParameterizedTest
@ValueSource(strings = [EMPTY_STRING, "second", "some_value"])
fun `should return fallback value when next response body is empty`(fallbackBody: String) {
val client = createRapidClient(createEmptyResponseEngine())
val firstResponse = Response(200, "first", mapOf("link" to listOf("<second>; rel=\"next\""), "pagination-total-results" to listOf("2")))

val paginator = ResponsePaginator(client, firstResponse, getBody)
val paginator = ResponsePaginator(client, firstResponse, EMPTY_STRING, getBody)
assertTrue(paginator.hasNext())
assertEquals("first", paginator.next().data)
assertTrue(paginator.hasNext())
assertEquals(EMPTY_STRING, paginator.next().data)
assertFalse(paginator.hasNext())
}

@Test
fun `should return empty value when next response body is empty and gzip encoded`() {
@ParameterizedTest
@ValueSource(strings = [EMPTY_STRING, "second", "some_value"])
fun `should return fallback value when next response body is empty and gzip encoded`(fallbackBody: String) {
val client = createRapidClient(createGzipEncodedEmptyResponseEngine())
val firstResponse = Response(200, "first", mapOf("link" to listOf("<second>; rel=\"next\""), "pagination-total-results" to listOf("2")))

val paginator = ResponsePaginator(client, firstResponse, getBody)
val paginator = ResponsePaginator(client, firstResponse, EMPTY_STRING, getBody)
assertTrue(paginator.hasNext())
assertEquals("first", paginator.next().data)
assertTrue(paginator.hasNext())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@ val mustacheHelpers =
val paginationHeaders = listOf("Pagination-Total-Results", "Link")
val availableHeaders = operation.responses.find { it.code == "200" }?.headers?.filter { it.baseName in paginationHeaders }
if (availableHeaders?.size == paginationHeaders.size) {
fragment.execute(writer)
val fallbackBody =
when {
operation.returnType.startsWith("kotlin.collections.List") -> "emptyList()"
operation.returnType.startsWith("kotlin.collections.Map") -> "emptyMap()"
operation.returnType.startsWith("kotlin.collections.Set") -> "emptySet()"
else -> ""
}

val context = mapOf("fallbackBody" to fallbackBody)
fragment.execute(context, writer)
}
}
},
Expand Down

0 comments on commit 24c82e3

Please sign in to comment.