Skip to content

Commit f888506

Browse files
committed
docs: address final-review polish for pagination unification
1 parent e910ff5 commit f888506

3 files changed

Lines changed: 48 additions & 8 deletions

File tree

docs/implementation-plan.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,8 @@ defaults (per Square: `FAIL_ON_UNKNOWN_PROPERTIES=false`, `WRITE_DATES_AS_TIMEST
372372

373373
### WU-9: Pagination primitives
374374

375+
> Superseded by #30 (pagination unification): `Page` is now an immutable value type, strategies return `PageInfo` (`nextRequest == null` = end of stream), and `SimplePage` was removed.
376+
375377
**Status: shipped.** `Page`, `Paginator`, `PaginationStrategy`, and the three strategies
376378
(`Cursor` / `PageNumber` / `LinkHeader`) are in `sdk-core/.../pagination`, alongside
377379
helper types `SimplePage` and `RequestRebuilder`. `Paginator` gained a `maxPages` safety cap

sdk-core/src/main/kotlin/org/dexpace/sdk/core/pagination/AsyncPaginator.kt

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,9 @@ public class AsyncPaginator<T>
371371
}
372372

373373
/**
374-
* Emits a page's items to the consumer, then schedules the next request. Returns
375-
* `true` to continue driving, `false` if the consumer threw (walk aborted).
374+
* Delivers a parsed page to the page sink (which may emit the page's items or the
375+
* whole page), then schedules the next request read from the [ParsedPage]. Returns
376+
* `true` to continue driving, `false` if the sink threw (walk aborted).
376377
*/
377378
private fun drainPage(page: ParsedPage<T>): Boolean {
378379
try {
@@ -386,9 +387,10 @@ public class AsyncPaginator<T>
386387
}
387388

388389
/**
389-
* Executes [request], parses the response into a [Page], and closes the response —
390-
* mirroring [Paginator]'s per-page lifecycle. The returned future completes with the
391-
* parsed page or exceptionally if the transport or strategy fails.
390+
* Executes [request], parses the response into a [PageInfo], snapshots a [Page], and
391+
* closes the response — mirroring [Paginator]'s per-page lifecycle. The returned future
392+
* completes with the resulting [ParsedPage] or exceptionally if the transport or
393+
* strategy fails.
392394
*/
393395
private fun fetchPage(request: Request): CompletableFuture<ParsedPage<T>> {
394396
pagesFetched++

sdk-core/src/test/kotlin/org/dexpace/sdk/core/pagination/PagedIterableTest.kt

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@ import org.dexpace.sdk.core.http.request.Request
1414
import org.dexpace.sdk.core.http.response.Response
1515
import org.dexpace.sdk.core.http.response.ResponseBody
1616
import org.dexpace.sdk.core.http.response.Status
17-
import org.dexpace.sdk.core.io.Io
18-
import org.dexpace.sdk.io.OkioIoProvider
1917
import java.io.IOException
2018
import java.net.URL
2119
import java.util.concurrent.atomic.AtomicInteger
@@ -28,7 +26,7 @@ import kotlin.test.assertSame
2826
import kotlin.test.assertTrue
2927

3028
class PagedIterableTest {
31-
@BeforeTest fun setup() = Io.installProvider(OkioIoProvider)
29+
@BeforeTest fun setup() = installIoProvider()
3230

3331
private fun request(): Request =
3432
Request.builder().url(URL("https://api.example.com/items")).method(Method.GET).build()
@@ -169,6 +167,44 @@ class PagedIterableTest {
169167
assertEquals(0, nextPageCalls.get(), "nextPage must not be called for an empty continuationToken")
170168
}
171169

170+
// -------------------------------------------------------------------------
171+
// Behavior 2b: continuationToken drives the next fetch when nextLink is null
172+
// -------------------------------------------------------------------------
173+
174+
@Test
175+
fun `continuationToken is used when nextLink is null and the next page is followed`() {
176+
val observedToken = arrayOfNulls<String>(1)
177+
val iterable =
178+
PagedIterable<Int>(
179+
firstPage = { page(listOf(1), nextLink = null, continuationToken = "tok") },
180+
nextPage = { _, token ->
181+
observedToken[0] = token
182+
page(listOf(2))
183+
},
184+
)
185+
assertEquals(listOf(1, 2), iterable.toList())
186+
assertEquals("tok", observedToken[0], "continuationToken must drive nextPage when nextLink is null")
187+
}
188+
189+
// -------------------------------------------------------------------------
190+
// Behavior 2c: a null return from nextPage terminates iteration
191+
// -------------------------------------------------------------------------
192+
193+
@Test
194+
fun `null return from nextPage terminates iteration after first page`() {
195+
val nextPageCalls = AtomicInteger(0)
196+
val iterable =
197+
PagedIterable<Int>(
198+
firstPage = { page(listOf(1, 2), nextLink = "p2") },
199+
nextPage = { _, _ ->
200+
nextPageCalls.incrementAndGet()
201+
null
202+
},
203+
)
204+
assertEquals(listOf(1, 2), iterable.toList())
205+
assertEquals(1, nextPageCalls.get(), "nextPage must be called exactly once before its null ends the stream")
206+
}
207+
172208
// -------------------------------------------------------------------------
173209
// Behavior 3: null first-page return yields nothing
174210
// -------------------------------------------------------------------------

0 commit comments

Comments
 (0)