Skip to content

Commit

Permalink
✨ add page utils
Browse files Browse the repository at this point in the history
  • Loading branch information
oharaandrew314 committed Feb 20, 2025
1 parent 272f474 commit 5a0ea98
Show file tree
Hide file tree
Showing 6 changed files with 129 additions and 1 deletion.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ Utilities for my kotlin microservices
- Result4k and Kotlin-Result migration
- Result4k failIf and recoverIf
- Exposed transforms for nullable columns
- Feature Flags (Static, Split, Evidently)
- Feature Flags (Static, Split, Evidently)
- Cursor-based pagination utils
9 changes: 9 additions & 0 deletions src/main/kotlin/dev/andrewohara/utils/pagination/Page.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package dev.andrewohara.utils.pagination

data class Page<Item: Any, Cursor: Any>(
val items: List<Item>,
val next: Cursor?
)



22 changes: 22 additions & 0 deletions src/main/kotlin/dev/andrewohara/utils/pagination/PageExtensions.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package dev.andrewohara.utils.pagination

fun <In: Any, Out: Any, Cursor: Any> Page<In, Cursor>.map(fn: (In) -> Out) = Page(
items = items.map(fn),
next = next
)

fun <Item: Any, Cursor: Any> Page<Item, Cursor>.filter(fn: (Item) -> Boolean) = Page(
items = items.filter(fn),
next = next
)

fun <Item: Any, Cursor: Any> stream(paginator: Paginator<Item, Cursor>) = sequence {
var cursor: Cursor? = null
do {
val page = paginator(cursor)
yieldAll(page.items)
cursor = page.next
} while (cursor != null)
}

fun <Item: Any, Cursor: Any> Paginator<Item, Cursor>.asSequence() = stream(this)
5 changes: 5 additions & 0 deletions src/main/kotlin/dev/andrewohara/utils/pagination/Paginator.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.andrewohara.utils.pagination

fun interface Paginator<Item: Any, Cursor: Any> {
operator fun invoke(cursor: Cursor?): Page<Item, Cursor>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.andrewohara.utils.pagination

data class FakeMessage(
val id: Int,
val topic: String,
val message: String
)

class FakePaginationRepo(private val pageSize: Int) {

private val messages = mutableListOf<FakeMessage>()

operator fun plusAssign(message: FakeMessage) = messages.plusAssign(message)

fun list(topic: String, cursor: Int?): Page<FakeMessage, Int> {
val results = messages
.sortedBy { it.id }
.filter { it.topic == topic }
.dropWhile { cursor != null && it.id < cursor }

return Page(
items = results.take(pageSize),
next = results.getOrNull(pageSize)?.id
)
}
}
65 changes: 65 additions & 0 deletions src/test/kotlin/dev/andrewohara/utils/pagination/PageTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package dev.andrewohara.utils.pagination

import io.kotest.matchers.sequences.shouldContainExactly
import io.kotest.matchers.shouldBe
import org.junit.jupiter.api.Test

class PageTest {

private val repo = FakePaginationRepo(pageSize = 2)
private val stuff1 = FakeMessage(1, topic = "stuff", message = "stuff1").also(repo::plusAssign)
private val thing1 = FakeMessage(2, topic = "things", message = "thing1").also(repo::plusAssign)
private val stuff2 = FakeMessage(3, topic = "stuff", message = "stuff2").also(repo::plusAssign)
private val stuff3 = FakeMessage(4, topic = "stuff", message = "stuff3").also(repo::plusAssign)

@Test
fun `repo sanity`() {
repo.list("things", null) shouldBe Page(
items = listOf(thing1),
next = null
)
repo.list("stuff", null) shouldBe Page(
items = listOf(stuff1, stuff2),
next = stuff3.id
)
repo.list("stuff", stuff3.id) shouldBe Page(
items = listOf(stuff3),
next = null
)
}

@Test
fun `paginator as sequence`() {
val paginator = Paginator { cursor: Int? -> repo.list("stuff", cursor) }
paginator.asSequence().shouldContainExactly(stuff1, stuff2, stuff3)
}

@Test
fun `filter page`() {
Page(
items = listOf(stuff1, thing1, stuff2, stuff3),
next = null
)
.filter { it.topic == "stuff" } shouldBe Page(
items = listOf(stuff1, stuff2, stuff3),
next = null
)
}

@Test
fun `map page`() {
Page(
items = listOf(stuff1, thing1, stuff2, stuff3),
next = null
)
.map { it.copy(topic = "all") } shouldBe Page(
items = listOf(
stuff1.copy(topic = "all"),
thing1.copy(topic = "all"),
stuff2.copy(topic = "all"),
stuff3.copy(topic = "all")
),
next = null
)
}
}

0 comments on commit 5a0ea98

Please sign in to comment.