Skip to content

Instantly share code, notes, and snippets.

@felix19350
Last active June 3, 2022 16:36
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save felix19350/82d03908e5f2149c718e03b1d094bb04 to your computer and use it in GitHub Desktop.
Save felix19350/82d03908e5f2149c718e03b1d094bb04 to your computer and use it in GitHub Desktop.
Ktor REST style pagination headers
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