Skip to content

Commit

Permalink
Reintegrate search pagination too, add lots more pagination testing
Browse files Browse the repository at this point in the history
  • Loading branch information
mzeitlin11 committed Dec 22, 2023
1 parent ccb9570 commit 00c139c
Show file tree
Hide file tree
Showing 97 changed files with 931 additions and 470 deletions.
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ async fn create_customer(client: &Client) -> Result<(), stripe::Error> {
```

The locations where such a migration is necessary are most easily found due to compiler errors on upgrading. Information
on determining the crate a request lives in can be found in the [README][README.md/Stripe\ Request\ Crates]. The
on determining the crate a request lives in can be found in the [README](README.md#stripe-request-crates). The
general steps will be:
1. Find the required crate and feature for the request by [searching here](crate_info.md)
2. Using `Account` and `create` as an example, convert the general structure:
Expand Down Expand Up @@ -129,7 +129,8 @@ breaking changes we missed here. If so, please open an issue (especially for cha
### Non-breaking Changes
- `List<>` types now are paginable. This allows usage such as paginating the external accounts returned
as `List<ExternalAccount>` from retrieving an account.
- The `smart-default` dependency was removed
- `SearchList<>` types are now paginable.
- The `smart-default` dependency was removed.

# [0.26.0](https://github.com/arlyon/async-stripe/compare/v0.25.2...v0.26.0) (2023-10-31)

Expand Down
2 changes: 1 addition & 1 deletion async-stripe/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ mod client;
mod error;
mod pagination;

pub use pagination::{ListPaginator, PaginationExt, PaginationParams};
pub use pagination::{ListPaginator, PaginationExt};
pub use stripe_shared::account::AccountId;
pub use stripe_shared::application::ApplicationId;
pub use stripe_shared::ApiVersion;
Expand Down
188 changes: 103 additions & 85 deletions async-stripe/src/pagination.rs
Original file line number Diff line number Diff line change
@@ -1,84 +1,110 @@
// Necessary under tokio-blocking since `Response` is a type alias to a `Result`
#![allow(clippy::missing_errors_doc)]
use serde::de::DeserializeOwned;
use serde::Serialize;
use stripe_types::{AsCursorOpt, List, Object};
use stripe_types::{List, PaginableList, SearchList};

use crate::{Client, Response};

/// Should only be implemented by `List*` parameter types. Kept public so that
/// the generated code crates can access this trait.
#[doc(hidden)]
pub trait PaginationParams: Serialize {}
use crate::Client;

#[derive(Debug)]
pub struct ListPaginator<T> {
data: Vec<T>,
url: String,
has_more: bool,
total_count: Option<u64>,
page: T,
params: serde_json::Value,
}

pub trait PaginationExt<T> {
fn into_paginator(self) -> ListPaginator<T>;
pub trait PaginationExt {
type Data;

fn into_paginator(self) -> ListPaginator<Self::Data>;
}

impl<T> PaginationExt<T> for List<T>
impl<T> PaginationExt for List<T>
where
T: Object + DeserializeOwned + Send + Sync + 'static,
T::Id: ToString,
T: Sync + Send + 'static,
List<T>: PaginableList,
{
fn into_paginator(self) -> ListPaginator<T> {
let mut paginator = ListPaginator {
data: self.data,
// the url we get back is prefixed
url: self.url.trim_start_matches("/v1/").to_string(),
has_more: self.has_more,
total_count: self.total_count,
params: Default::default(),
};
if let Some(curr_cursor) = paginator.data.last().and_then(|t| t.id().as_cursor_opt()) {
paginator.update_cursor(curr_cursor.to_string());
}
paginator
type Data = List<T>;

fn into_paginator(mut self) -> ListPaginator<List<T>> {
let mut params = Default::default();
self.update_params(&mut params);
ListPaginator { page: self, params }
}
}

impl<T> ListPaginator<T> {
/// Kept public so that the generated code crates can access this trait. Used by `List*` params
/// to implement `PaginationExt`.
impl<T> PaginationExt for SearchList<T>
where
T: Sync + Send + 'static,
SearchList<T>: PaginableList,
{
type Data = SearchList<T>;

fn into_paginator(mut self) -> ListPaginator<SearchList<T>> {
let mut params = Default::default();
self.update_params(&mut params);
ListPaginator { page: self, params }
}
}

impl<T> ListPaginator<SearchList<T>> {
/// Kept public so that the generated code crates can access this trait. Used by `Search*` params
/// to implement `PaginationExt`. Not part of the public API.
#[doc(hidden)]
pub fn from_params(url: &str, params: impl PaginationParams) -> Self {
ListPaginator {
data: vec![],
pub fn from_search_params(url: &str, params: impl Serialize) -> Self {
let page = SearchList {
url: url.to_string(),
has_more: true,
data: vec![],
next_page: None,
total_count: None,
params: serde_json::to_value(params).expect("Invalid pagination params"),
};
Self {
page,
params: serde_json::to_value(params)
// Expect should be safe since we control which types call this
.expect("all Stripe types implement `Serialize` infallibly"),
}
}
}

impl<T> ListPaginator<List<T>> {
/// Kept public so that the generated code crates can access this trait. Used by `List*` params
/// to implement `PaginationExt`. Not part of the public API.
#[doc(hidden)]
pub fn from_list_params(url: &str, params: impl Serialize) -> Self {
let page = List { data: vec![], has_more: true, total_count: None, url: url.to_string() };
Self {
page,
params: serde_json::to_value(params)
.expect("all Stripe types implement `Serialize` infallibly"),
}
}
}

impl<T> ListPaginator<T>
where
T: Object + DeserializeOwned + Send + Sync + 'static,
T: PaginableList + Send + Sync + 'static,
{
/// Repeatedly queries Stripe for more data until all elements in list are fetched, using
/// Stripe's default page size.
///
/// Requires `feature = "blocking"`.
#[cfg(feature = "blocking")]
pub fn get_all(self, client: &Client) -> Response<Vec<T>> {
let mut data = Vec::with_capacity(self.total_count.unwrap_or(0) as usize);
let mut paginator = self;
pub fn get_all(self, client: &Client) -> crate::Response<Vec<T::Data>> {
let mut data = vec![];
let mut parts = self.page.into_parts();
let mut params = self.params;
loop {
if !paginator.has_more {
data.extend(paginator.data);
// `append` empties `parts.data`
data.append(&mut parts.data);

if !parts.has_more {
break;
}
let next_page = paginator.fetch_page_with_curr_params(client)?;
paginator.update_with_new_data(next_page);

let mut next_page: T =
client.get_query(parts.url.trim_start_matches("/v1/"), &params)?;
next_page.update_params(&mut params);
parts = next_page.into_parts();
}
Ok(data)
}
Expand All @@ -92,67 +118,59 @@ where
/// Requires `feature = ["async", "stream"]`.
#[cfg(all(feature = "async", feature = "stream"))]
pub fn stream(
mut self,
self,
client: &Client,
) -> impl futures_util::Stream<Item = Result<T, crate::StripeError>> + Unpin {
) -> impl futures_util::Stream<Item = Result<T::Data, crate::StripeError>> + Unpin {
// We are going to be popping items off the end of the list, so we need to reverse it.
self.data.reverse();

Box::pin(futures_util::stream::unfold(Some((self, client.clone())), Self::unfold_stream))
let mut page = self.page.into_parts();
page.data.reverse();
let paginator = ListPaginator { page: T::from_parts(page), params: self.params };

Box::pin(futures_util::stream::unfold(
Some((paginator, client.clone())),
Self::unfold_stream,
))
}

/// unfold a single item from the stream
#[cfg(all(feature = "async", feature = "stream"))]
async fn unfold_stream(
state: Option<(Self, Client)>,
) -> Option<(Result<T, crate::StripeError>, Option<(Self, Client)>)> {
let (mut paginator, client) = state?; // If none, we sent the last item in the last iteration

if let Some(next_val) = paginator.data.pop() {
) -> Option<(Result<T::Data, crate::StripeError>, Option<(Self, Client)>)> {
let (paginator, client) = state?; // If none, our last request was an error, so stop here
let mut parts = paginator.page.into_parts();
if let Some(next_val) = parts.data.pop() {
// We have more data on this page
return Some((Ok(next_val), Some((paginator, client))));
return Some((
Ok(next_val),
Some((Self { page: T::from_parts(parts), params: paginator.params }, client)),
));
}

// Final value of the stream, no errors
if !paginator.has_more {
if !parts.has_more {
return None;
}

match paginator.fetch_page_with_curr_params(&client).await {
Ok(next_page) => {
debug_assert!(paginator.data.is_empty());
paginator.update_with_new_data(next_page);
match client
.get_query::<T, _>(parts.url.trim_start_matches("/v1/"), &paginator.params)
.await
{
Ok(mut next_page) => {
let mut params = paginator.params;
next_page.update_params(&mut params);

let mut parts = next_page.into_parts();

// We are going to be popping items off the end of the list, so we need to reverse it.
// The assert above ensures we are only reversing this specific list we've
// just received
paginator.data.reverse();
parts.data.reverse();

let next_val = paginator.data.pop()?;
let next_val = parts.data.pop()?;

// Yield last value of this page, the next page (and client) becomes the state
Some((Ok(next_val), Some((paginator, client))))
Some((Ok(next_val), Some((Self { page: T::from_parts(parts), params }, client))))
}
Err(e) => Some((Err(e), None)), // We ran into an error. The last value of the stream will be the error.
}
}

fn fetch_page_with_curr_params(&self, client: &Client) -> Response<List<T>> {
client.get_query(&self.url, &self.params)
}

fn update_cursor(&mut self, id: String) {
self.params["starting_after"] = serde_json::Value::String(id);
}

fn update_with_new_data(&mut self, list: List<T>) {
self.has_more = list.has_more;
self.total_count = list.total_count;
if let Some(new_cursor) = list.data.last().and_then(|l| l.id().as_cursor_opt()) {
self.update_cursor(new_cursor.to_string());
} else {
self.has_more = false;
Err(err) => Some((Err(err), None)), // We ran into an error. The last value of the stream will be the error.
}
self.data.extend(list.data);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,12 @@ impl<'a> ListBillingPortalConfiguration<'a> {
) -> stripe::Response<stripe_types::List<stripe_billing::BillingPortalConfiguration>> {
client.get_query("/billing_portal/configurations", self)
}
pub fn paginate(self) -> stripe::ListPaginator<stripe_billing::BillingPortalConfiguration> {
stripe::ListPaginator::from_params("/billing_portal/configurations", self)
pub fn paginate(
self,
) -> stripe::ListPaginator<stripe_types::List<stripe_billing::BillingPortalConfiguration>> {
stripe::ListPaginator::from_list_params("/billing_portal/configurations", self)
}
}
impl<'a> stripe::PaginationParams for ListBillingPortalConfiguration<'a> {}
#[derive(Copy, Clone, Debug, serde::Serialize)]
pub struct CreateBillingPortalConfiguration<'a> {
/// The business information shown to customers in the portal.
Expand Down
12 changes: 6 additions & 6 deletions generated/stripe_billing/src/credit_note/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -616,11 +616,10 @@ impl<'a> ListCreditNote<'a> {
) -> stripe::Response<stripe_types::List<stripe_shared::CreditNote>> {
client.get_query("/credit_notes", self)
}
pub fn paginate(self) -> stripe::ListPaginator<stripe_shared::CreditNote> {
stripe::ListPaginator::from_params("/credit_notes", self)
pub fn paginate(self) -> stripe::ListPaginator<stripe_types::List<stripe_shared::CreditNote>> {
stripe::ListPaginator::from_list_params("/credit_notes", self)
}
}
impl<'a> stripe::PaginationParams for ListCreditNote<'a> {}
#[derive(Copy, Clone, Debug, Default, serde::Serialize)]
pub struct UpdateCreditNote<'a> {
/// Specifies which fields in the response should be expanded.
Expand Down Expand Up @@ -974,8 +973,9 @@ impl<'a> PreviewLinesCreditNote<'a> {
) -> stripe::Response<stripe_types::List<stripe_shared::CreditNoteLineItem>> {
client.get_query("/credit_notes/preview/lines", self)
}
pub fn paginate(self) -> stripe::ListPaginator<stripe_shared::CreditNoteLineItem> {
stripe::ListPaginator::from_params("/credit_notes/preview/lines", self)
pub fn paginate(
self,
) -> stripe::ListPaginator<stripe_types::List<stripe_shared::CreditNoteLineItem>> {
stripe::ListPaginator::from_list_params("/credit_notes/preview/lines", self)
}
}
impl<'a> stripe::PaginationParams for PreviewLinesCreditNote<'a> {}
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ impl<'a> ListCreditNoteLineItem<'a> {
pub fn paginate(
self,
credit_note: &stripe_shared::credit_note::CreditNoteId,
) -> stripe::ListPaginator<stripe_shared::CreditNoteLineItem> {
stripe::ListPaginator::from_params(&format!("/credit_notes/{credit_note}/lines"), self)
) -> stripe::ListPaginator<stripe_types::List<stripe_shared::CreditNoteLineItem>> {
stripe::ListPaginator::from_list_params(&format!("/credit_notes/{credit_note}/lines"), self)
}
}
impl<'a> stripe::PaginationParams for ListCreditNoteLineItem<'a> {}
17 changes: 11 additions & 6 deletions generated/stripe_billing/src/invoice/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ impl<'a> SearchInvoice<'a> {
) -> stripe::Response<stripe_types::SearchList<stripe_shared::Invoice>> {
client.get_query("/invoices/search", self)
}
pub fn paginate(
self,
) -> stripe::ListPaginator<stripe_types::SearchList<stripe_shared::Invoice>> {
stripe::ListPaginator::from_search_params("/invoices/search", self)
}
}
#[derive(Copy, Clone, Debug, Default, serde::Serialize)]
pub struct UpcomingInvoice<'a> {
Expand Down Expand Up @@ -4485,11 +4490,12 @@ impl<'a> UpcomingLinesInvoice<'a> {
) -> stripe::Response<stripe_types::List<stripe_shared::InvoiceLineItem>> {
client.get_query("/invoices/upcoming/lines", self)
}
pub fn paginate(self) -> stripe::ListPaginator<stripe_shared::InvoiceLineItem> {
stripe::ListPaginator::from_params("/invoices/upcoming/lines", self)
pub fn paginate(
self,
) -> stripe::ListPaginator<stripe_types::List<stripe_shared::InvoiceLineItem>> {
stripe::ListPaginator::from_list_params("/invoices/upcoming/lines", self)
}
}
impl<'a> stripe::PaginationParams for UpcomingLinesInvoice<'a> {}
#[derive(Copy, Clone, Debug, Default, serde::Serialize)]
pub struct CreateInvoice<'a> {
/// The account tax IDs associated with the invoice.
Expand Down Expand Up @@ -6715,11 +6721,10 @@ impl<'a> ListInvoice<'a> {
) -> stripe::Response<stripe_types::List<stripe_shared::Invoice>> {
client.get_query("/invoices", self)
}
pub fn paginate(self) -> stripe::ListPaginator<stripe_shared::Invoice> {
stripe::ListPaginator::from_params("/invoices", self)
pub fn paginate(self) -> stripe::ListPaginator<stripe_types::List<stripe_shared::Invoice>> {
stripe::ListPaginator::from_list_params("/invoices", self)
}
}
impl<'a> stripe::PaginationParams for ListInvoice<'a> {}
#[derive(Copy, Clone, Debug, Default, serde::Serialize)]
pub struct RetrieveInvoice<'a> {
/// Specifies which fields in the response should be expanded.
Expand Down
5 changes: 2 additions & 3 deletions generated/stripe_billing/src/invoice_item/requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,10 @@ impl<'a> ListInvoiceItem<'a> {
) -> stripe::Response<stripe_types::List<stripe_shared::InvoiceItem>> {
client.get_query("/invoiceitems", self)
}
pub fn paginate(self) -> stripe::ListPaginator<stripe_shared::InvoiceItem> {
stripe::ListPaginator::from_params("/invoiceitems", self)
pub fn paginate(self) -> stripe::ListPaginator<stripe_types::List<stripe_shared::InvoiceItem>> {
stripe::ListPaginator::from_list_params("/invoiceitems", self)
}
}
impl<'a> stripe::PaginationParams for ListInvoiceItem<'a> {}
#[derive(Copy, Clone, Debug, serde::Serialize)]
pub struct CreateInvoiceItem<'a> {
/// The integer amount in cents (or local equivalent) of the charge to be applied to the upcoming invoice.
Expand Down
Loading

0 comments on commit 00c139c

Please sign in to comment.