page-turner 1.0.0

A generic abstraction of APIs with pagination
Documentation
![Build](https://github.com/a1akris/page-turner/actions/workflows/build.yml/badge.svg)

# page-turner

A generic abstraction of paginated APIs

## In a nutshell

If you have a paginated request implement a page turner trait for this request
on your client:

```rust
use page_turner::prelude::*;

impl PageTurner<GetReviewsRequest> for ApiClient {
    type PageItems = Vec<Review>;
    type PageError = ApiError;

    async fn turn_page(&self, request: GetReviewsRequest) -> TurnedPageResult<Self, GetReviewsRequest> {
        let response = self.execute(request).await?;

        let turned_page = match response.next_page_token {
            Some(token) => TurnedPage::next(response.reviews, GetReviewsRequest::from(token)),
            None => TurnedPage::last(response.reviews),
        };

        Ok(turned_page)
    }
}
```

The page turner then provides stream-based APIs that allow data to be queried
as if pagination does not exist:

```rust
use page_turner::prelude::*;
use futures::{StreamExt, TryStreamExt};

async fn first_n_positive_reviews(client: &ApiClient, request: GetReviewsRequest, count: usize) -> ApiResult<Vec<Review>> {
    client
        .pages(request)
        .items()
        .try_filter(|review| std::future::ready(review.is_positive()))
        .take(count)
        .await
}

```

Both cursor and non-cursor pagination patterns are supported and for the later
one you can enable a concurrent querying by implementing the `RequestAhead`
trait:

```rust
pub struct GetCommentsRequest {
    pub page_id: PageId,
}

impl RequestAhead for GetCommentsRequest {
    fn next_request(&self) -> Self {
        Self {
            page_id: self.page_id + 1,
        }
    }
}
```

Now you can use `pages_ahead`/`pages_ahead_unordered` family of methods to
request multiple pages concurrently using a quite optimal [sliding
window](https://docs.rs/page-turner/1.0.0/page_turner/mt/trait.PageTurner.html#method.pages_ahead)
request scheduling under the hood:

```rust
use page_turner::prelude::*;
use futures::TryStreamExt;

async fn retrieve_user_comments(username: &str) -> ResponseResult<Vec<Comment>> {
    let client = ForumClient::new();

    client.pages_ahead(4, Limit::None, GetCommentsRequest { page_id: 1 })
        .items()
        .try_filter(|comment| std::future::ready(comment.author == username))
        .try_collect()
        .await

}
```

The example above schedules requests for 4 pages simultaneously and then issues
a request as soon as you receive a response concurrently awaiting for 4
responses all the time while you're processing results.


## v1.0.0 release

The `v1.0.0` uses features like RPITIT stabilized in Rust 1.75, so MSRV for
`v1.0.0` is `1.75.0`. If you can't afford to upgrade to Rust `1.75` use `0.8.2`
version of the crate. It's quite similar and supports Rust versions the
`async_trait` crate supports.

See [docs](https://docs.rs/page-turner) for details about new supported
features.

See [CHANGELOG.md](CHANGELOG.md) for full changes history.


### Migration to v1.0.0 from older versions

There are several major breaking changes in v1.0.0, here are instructions how
to quickly adopt them:

1. No more `#[async_trait]` by default. Remove `#[async_trait]` from your page
   turner impls and everything should work. If you for some reason rely on `dyn
   PageTurner` then enable feature `dynamic` and use
   `page_turner::dynamic::prelude::*` instead of the `page_turner::prelude::*`.

1. New page turners don't enforce you to return `Vec<PageItem>` anymore, now
   you can return whatever you like(`HashMap` is a one example of a popular
   alternative). To quickly make your code compile retaining the old behavior
   replace `type PageItem = YourItem;` with `type PageItems = Vec<YourItem>;`.
   Note that `s` in `PageItems` :)

1. `PageTurnerOutput` was renamed into `TurnedPageResult` but it is the same
   type alias so a simple global search&replace should do the trick.

1. `into_pages_ahead` and `into_pages_ahead_unordered` methods now require
   implementors to be clonable. Previously, they used `Arc` under the hood, but
   now it's up to you. Most likely your clients are already cheaply clonable
   but if not then the quickest way to fix `doesn't implement Clone` errors is
   to wrap your clients into `Arc` like
   `Arc::new(client).into_pages_ahead(..)`.


##### License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT
license](LICENSE-MIT) at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted
for inclusion in this crate by you, as defined in the Apache-2.0 license, shall
be dual licensed as above, without any additional terms or conditions.