Last active
June 3, 2022 16:36
-
-
Save felix19350/82d03908e5f2149c718e03b1d094bb04 to your computer and use it in GitHub Desktop.
Ktor REST style pagination headers
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import io.ktor.application.ApplicationCall | |
import io.ktor.http.URLBuilder | |
import io.ktor.http.takeFrom | |
import io.ktor.util.createFromCall | |
import io.ktor.application.call | |
import io.ktor.http.HttpStatusCode | |
import io.ktor.request.receive | |
import io.ktor.response.respond | |
import io.ktor.routing.* | |
// Data class that represents the custom headers that will be generated by the PaginationHeaderGenerator. | |
data class PaginationHeader(val name: String, val value: String) | |
object PaginationHeaderGenerator { | |
private const val prefix = "vnd.myApp.pagination" | |
const val totalCountHeader = "$prefix.totalCount" | |
const val pageSizeHeader = "$prefix.pageSize" | |
const val currentPageHeader = "$prefix.currentPage" | |
const val nextPageHeader = "$prefix.nextPage" | |
const val nextPageUrlHeader = "$prefix.nextPageUrl" | |
const val previousPageHeader = "$prefix.previousPage" | |
const val previousPageUrlHeader = "$prefix.previousPageUrl" | |
fun buildHeaders(total: Long, | |
limit: Int, | |
offset: Int, | |
call: ApplicationCall, | |
limitParamName: String = "limit", | |
offsetParamName: String = "offset"): List<PaginationHeader> { | |
if (offset < 0) { | |
throw IllegalArgumentException("The offset must be zero or a positive integer") | |
} | |
return if (limit <= 0) { | |
buildHeadersForSinglePageDataSet(total) | |
} else { | |
buildHeadersForPagedDataSet(total, limit, offset, call, limitParamName, offsetParamName) | |
} | |
} | |
private fun buildHeadersForSinglePageDataSet(total: Long): List<PaginationHeader> { | |
return listOf( | |
PaginationHeader(totalCountHeader, total.toString()), | |
PaginationHeader(pageSizeHeader, total.toString()), | |
PaginationHeader(currentPageHeader, 1.toString()) | |
) | |
} | |
private fun buildHeadersForPagedDataSet(total: Long, | |
limit: Int, | |
offset: Int, | |
call: ApplicationCall, | |
limitParamName: String, | |
offsetParamName: String): List<PaginationHeader> { | |
val currentPageNumber = 1 + Math.ceil(offset / limit.toDouble()).toInt() | |
val baseUrl = URLBuilder.createFromCall(call) | |
baseUrl.parameters.remove(limitParamName) | |
baseUrl.parameters.remove(offsetParamName) | |
val headers = mutableListOf( | |
PaginationHeader(totalCountHeader, total.toString()), | |
PaginationHeader(pageSizeHeader, limit.toString()), | |
PaginationHeader(currentPageHeader, currentPageNumber.toString())) | |
val hasPreviousPage = currentPageNumber > 1 | |
val lowerBound = offset - limit | |
if (hasPreviousPage) { | |
val prevUrl = URLBuilder().takeFrom(baseUrl) | |
prevUrl.parameters.append(limitParamName, limit.toString()) | |
prevUrl.parameters.append(offsetParamName, lowerBound.toString()) | |
headers.add(PaginationHeader(previousPageHeader, (currentPageNumber - 1).toString())) | |
headers.add(PaginationHeader(previousPageUrlHeader, prevUrl.buildString())) | |
} | |
val upperBound = limit + offset | |
val hasNextPage = total > upperBound | |
if (hasNextPage) { | |
val nextUrl = URLBuilder().takeFrom(baseUrl) | |
nextUrl.parameters.append(limitParamName, limit.toString()) | |
nextUrl.parameters.append(offsetParamName, upperBound.toString()) | |
headers.add(PaginationHeader(nextPageHeader, (currentPageNumber + 1).toString())) | |
headers.add(PaginationHeader(nextPageUrlHeader, nextUrl.buildString())) | |
} | |
return headers | |
} | |
} | |
/** | |
* Usage example: return a paginated list of MyResource instances. The pagination details (e.g. next and previous links) are | |
* kept in the response headers, and therefore the response payload is as clean as possible. | |
*/ | |
fun Route.myRoute(repo: MyResourceRepository) { | |
get("/my-resource") { | |
val limit = call.parameters["limit"]?.toInt() ?: -1 | |
val offset = call.parameters["offset"]?.toInt() ?: 0 | |
val satDefList = repo.list(limit, offset) | |
PaginationHeaderGenerator.buildHeaders(repo.count(), limit, offset, call).forEach { header -> | |
call.response.headers.append(header.name, header.value) | |
} | |
call.respond(satDefList) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment