Crate page_turner
source ·Expand description
A generic abstraction of paginated APIs
Imagine, you need to use the following API to find the most upvoted comment under a blog post.
struct GetCommentsRequest {
blog_post_id: BlogPostId,
page_number: u32,
}
struct GetCommentsResponse {
comments: Vec<Comment>,
more_comments_available: bool,
}
In order to do that you will need to write a hairy loop that checks the
more_comments_available
flag, increments page_number
, and updates a
variable that stores the resulting value. This crate helps to abstract away any
paginated API and allows you to work with such APIs uniformly with the help of
async streams. Both cursor and non-cursor pagniations are supported. All you
need to do is to implement the PageTurner
trait for the client that sends
GetCommentsRequest
.
In PageTurner
you specify what data you fetch and what errors may occur for
a particular request, then you implement the turn_page
method where you
describe how to query a single page and how to prepare a request for the next
page.
use page_turner::prelude::*;
impl PageTurner<GetCommentsRequest> for BlogClient {
type PageItems = Vec<Comment>;
type PageError = BlogClientError;
async fn turn_page(&self, mut request: GetCommentsRequest) -> TurnedPageResult<Self, GetCommentsRequest> {
let response = self.get_comments(request.clone()).await?;
if response.more_comments_available {
request.page_number += 1;
Ok(TurnedPage::next(response.comments, request))
} else {
Ok(TurnedPage::last(response.comments))
}
}
}
PageTurner
then provides default implementations for PageTurner::pages
and PageTurner::into_pages
methods that you can use to get a stream of
pages and, optionally, to turn it into a stream of page items if you need. Now
we can use our client to find the most upvoted comment like that:
let client = BlogClient::new();
let most_upvoted_comment = client
.pages(GetCommentsRequest { blog_post_id, page_number: 1 })
.items()
.try_fold(None::<Comment>, |most_upvoted, next_comment| async move {
match most_upvoted {
Some(comment) if next_comment.upvotes > comment.upvotes => Ok(Some(next_comment)),
current @ Some(_) => Ok(current),
None => Ok(Some(next_comment)),
}
})
.await?
.unwrap();
assert_eq!(most_upvoted_comment.text, "Yeet");
assert_eq!(most_upvoted_comment.upvotes, 5);
// Or we can process the whole pages if needed
let mut comment_pages = std::pin::pin!(client.pages(GetCommentsRequest { blog_post_id, page_number: 1 }));
while let Some(comment_page) = comment_pages.try_next().await? {
detect_spam(comment_page);
}
Notice, that with this kind of the API we don’t require any data from response
to construct the next valid request. We can take an advantage on such APIs by
implementing the RequestAhead
trait on a request type. For requests that
implement RequestAhead
PageTurner
provides additional methods -
PageTurner::pages_ahead
and PageTurner::pages_ahead_unordered
. These
methods allow to query multiple pages concurrently using an optimal sliding
window request scheduling.
impl RequestAhead for GetCommentsRequest {
fn next_request(&self) -> Self {
Self {
blog_post_id: self.blog_post_id,
page_number: self.page_number + 1,
}
}
}
let client = BlogClient::new();
// Now instead of querying pages one by one we make 4 concurrent requests
// for multiple pages under the hood but besides using a different PageTurner
// method nothing changes in the user code.
let most_upvoted_comment = client
.pages_ahead(4, Limit::None, GetCommentsRequest { blog_post_id, page_number: 1 })
.items()
.try_fold(None::<Comment>, |most_upvoted, next_comment| async move {
match most_upvoted {
Some(comment) if next_comment.upvotes > comment.upvotes => Ok(Some(next_comment)),
current @ Some(_) => Ok(current),
None => Ok(Some(next_comment)),
}
})
.await?
.unwrap();
assert_eq!(most_upvoted_comment.text, "Yeet");
assert_eq!(most_upvoted_comment.upvotes, 5);
// In the example above the order of pages being returned corresponds to the order
// of requests which means the stream is blocked until the first page is ready
// even if the second and the third pages are already received. For this use case
// we don't really care about the order of the comments so we can use
// pages_ahead_unordered to unblock the stream as soon as we receive a response to
// any of the concurrent requests.
let most_upvoted_comment = client
.pages_ahead_unordered(4, Limit::None, GetCommentsRequest { blog_post_id, page_number: 1 })
.items()
.try_fold(None::<Comment>, |most_upvoted, next_comment| async move {
match most_upvoted {
Some(comment) if next_comment.upvotes > comment.upvotes => Ok(Some(next_comment)),
current @ Some(_) => Ok(current),
None => Ok(Some(next_comment)),
}
})
.await?
.unwrap();
assert_eq!(most_upvoted_comment.text, "Yeet");
assert_eq!(most_upvoted_comment.upvotes, 5);
Page turner flavors and crate features
There are multiple falvors of page turners suitable for different contexts and
each flavor is available behind a feature flag. By default the mt
feature is
enabled which provides you crate::mt::PageTurner
that works with
multithreaded executors. The crate root prelude
just reexports the
crate::mt::prelude::*
, and types at the crate root are reexported from
crate::mt
. Overall there are the following page turner flavors and
corresponding features available:
- local: Provides a less constrained
crate::local::PageTurner
suitable for singlethreaded executors. Usepage_turner::local::prelude::*
to work with it. - mutable: A
crate::mutable::PageTurner
is likelocal
but even allows your client to mutate during the request execution. Usepage_turner::mutable::prelude::*
to work with it. - mt: A
crate::mt::PageTurner
for multithreaded executors. It’s reexported by default but if you enable additional features in the same project it is recommended to usepage_turner::mt::prelude::*
to distinguish between different flavors of page turners. - dynamic: An object safe
crate::dynamic::PageTurner
that requiresasync_trait
to be implemented and can be used as an object with dynamic dispatch.
Re-exports
Modules
- local
local
A page turner suitable for singlethreaded executors. Seemutable
for a version that allows to use &mut self in methods - A page turner suitable for multithreaded executors. This is what you need in most cases. See
dynamic
if you also needdyn PageTurner
objects for some reason. - Minimal reexports you need to work with a page turner.
Structs
- A struct that combines items queried for the current page and an optional request to query the next page. If
next_request
isNone
PageTurner
stops querying pages.
Enums
- If you use
pages_ahead
orpages_ahead_unordered
families of methods and you know in advance how many pages you need to query, specifyLimit::Pages
to prevent redundant querying past the last existing page from being executed.
Traits
- If a request for the next page doesn’t require any data from the response and can be made out of the request for the current page implement this trait to enable
pages_ahead
,pages_ahead_unordered
families of methods that query pages concurrently.