stripe/
params.rs

1use std::collections::HashMap;
2use std::fmt::Display;
3
4use serde::de::DeserializeOwned;
5use serde::{Deserialize, Serialize};
6
7use crate::error::StripeError;
8use crate::resources::{ApiVersion, Currency};
9use crate::{
10    client::{
11        config::{err, ok},
12        Client, Response,
13    },
14    AccountId, ApplicationId,
15};
16
17#[derive(Clone, Default)]
18pub struct AppInfo {
19    pub name: String,
20    pub url: Option<String>,
21    pub version: Option<String>,
22}
23
24impl Display for AppInfo {
25    /// Formats a plugin's 'App Info' that can be added to the end of a User-Agent string.
26    ///
27    /// This formatting matches that of other libraries, and if changed then it should be changed everywhere.
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        match (&self.version, &self.url) {
30            (Some(a), Some(b)) => write!(f, "{}/{} ({})", &self.name, a, b),
31            (Some(a), None) => write!(f, "{}/{}", &self.name, a),
32            (None, Some(b)) => write!(f, "{} ({})", &self.name, b),
33            _ => write!(f, "{}", self.name),
34        }
35    }
36}
37
38#[derive(Clone)]
39pub struct Headers {
40    pub stripe_version: ApiVersion,
41    pub user_agent: String,
42
43    pub client_id: Option<ApplicationId>,
44    pub stripe_account: Option<AccountId>,
45}
46
47impl Headers {
48    pub fn to_array(&self) -> [(&str, Option<&str>); 4] {
49        [
50            ("Client-Id", self.client_id.as_deref()),
51            ("Stripe-Account", self.stripe_account.as_deref()),
52            ("Stripe-Version", Some(self.stripe_version.as_str())),
53            ("User-Agent", Some(&self.user_agent)),
54        ]
55    }
56}
57
58/// Implemented by types which represent stripe objects.
59pub trait Object {
60    /// The canonical id type for this object.
61    type Id;
62    /// The id of the object.
63    fn id(&self) -> Self::Id;
64    /// The object's type, typically represented in wire format as the `object` property.
65    fn object(&self) -> &'static str;
66}
67
68/// A deleted object.
69#[derive(Clone, Debug, Deserialize, Serialize)]
70pub struct Deleted<T> {
71    /// Unique identifier for the object.
72    pub id: T,
73    /// Always true for a deleted object.
74    pub deleted: bool,
75}
76
77/// The `Expand` struct is used to serialize `expand` arguments in retrieve and list apis.
78#[doc(hidden)]
79#[derive(Serialize)]
80pub struct Expand<'a> {
81    #[serde(skip_serializing_if = "Expand::is_empty")]
82    pub expand: &'a [&'a str],
83}
84
85impl Expand<'_> {
86    pub(crate) fn is_empty(expand: &[&str]) -> bool {
87        expand.is_empty()
88    }
89}
90
91/// An id or object.
92///
93/// By default stripe will return an id for most fields, but if more detail is
94/// necessary the `expand` parameter can be provided to ask for the id to be
95/// loaded as an object instead:
96///
97/// ```rust,ignore
98/// Charge::retrieve(&client, &charge_id, &["invoice.customer"])
99/// ```
100///
101/// For more details see <https://stripe.com/docs/api/expanding_objects>.
102#[derive(Clone, Debug, Serialize, Deserialize)] // TODO: Implement deserialize by hand for better error messages
103#[serde(untagged)]
104pub enum Expandable<T: Object> {
105    Id(T::Id),
106    Object(Box<T>),
107}
108
109impl<T> Expandable<T>
110where
111    T: Object,
112    T::Id: Clone + Default,
113{
114    pub fn id(&self) -> T::Id {
115        match self {
116            Expandable::Id(id) => id.clone(),
117            Expandable::Object(obj) => obj.id(),
118        }
119    }
120}
121
122impl<T: Object> Default for Expandable<T>
123where
124    T::Id: Default,
125{
126    fn default() -> Self {
127        Expandable::Id(Default::default())
128    }
129}
130
131impl<T: Object> Expandable<T> {
132    pub fn is_object(&self) -> bool {
133        match self {
134            Expandable::Id(_) => false,
135            Expandable::Object(_) => true,
136        }
137    }
138
139    pub fn as_object(&self) -> Option<&T> {
140        match self {
141            Expandable::Id(_) => None,
142            Expandable::Object(obj) => Some(obj),
143        }
144    }
145
146    pub fn into_object(self) -> Option<T> {
147        match self {
148            Expandable::Id(_) => None,
149            Expandable::Object(obj) => Some(*obj),
150        }
151    }
152}
153
154/// Implemented by types which support cursor-based pagination,
155/// typically with an id, allowing them to be fetched using a `List`
156/// returned by the corresponding "list" api request.
157pub trait Paginate {
158    type Cursor: AsCursor;
159    fn cursor(&self) -> Self::Cursor;
160}
161
162pub trait AsCursor: AsRef<str> {}
163
164impl<'a> AsCursor for &'a str {}
165impl AsCursor for String {}
166
167impl<T> Paginate for T
168where
169    T: Object,
170    T::Id: AsCursor,
171{
172    type Cursor = T::Id;
173    fn cursor(&self) -> Self::Cursor {
174        self.id()
175    }
176}
177
178pub trait Paginable {
179    type O: Object + Send;
180    fn set_last(&mut self, item: Self::O);
181}
182
183pub trait PaginableList {
184    type O: Paginate + DeserializeOwned + Send + Sync + 'static + Clone + std::fmt::Debug;
185    fn new(data: Vec<Self::O>, url: String, has_more: bool, total_count: Option<u64>) -> Self;
186    fn get_data_mut(&mut self) -> &mut Vec<Self::O>;
187    fn get_data(&self) -> &Vec<Self::O>;
188    fn get_url(&self) -> String;
189    fn get_total_count(&self) -> Option<u64>;
190    fn has_more(&self) -> bool;
191}
192
193/// A single page of a cursor-paginated list of a search object.
194///
195/// For more details, see <https://stripe.com/docs/api/pagination/search>
196#[derive(Debug, Clone, Deserialize, Serialize)]
197pub struct SearchList<T> {
198    pub object: String,
199    pub url: String,
200    pub has_more: bool,
201    pub data: Vec<T>,
202    pub next_page: Option<String>,
203    pub total_count: Option<u64>,
204}
205
206impl<T> Default for SearchList<T> {
207    fn default() -> Self {
208        SearchList {
209            object: String::new(),
210            data: Vec::new(),
211            has_more: false,
212            total_count: None,
213            url: String::new(),
214            next_page: None,
215        }
216    }
217}
218
219impl<T: Paginate + DeserializeOwned + Send + Sync + 'static + Clone + std::fmt::Debug> PaginableList
220    for SearchList<T>
221{
222    type O = T;
223
224    fn new(
225        data: Vec<Self::O>,
226        url: String,
227        has_more: bool,
228        total_count: Option<u64>,
229    ) -> SearchList<T> {
230        Self { object: "".to_string(), url, has_more, data, next_page: None, total_count }
231    }
232
233    fn get_data_mut(&mut self) -> &mut Vec<Self::O> {
234        &mut self.data
235    }
236
237    fn get_data(&self) -> &Vec<Self::O> {
238        &self.data
239    }
240    fn get_url(&self) -> String {
241        self.url.clone()
242    }
243    fn get_total_count(&self) -> Option<u64> {
244        self.total_count
245    }
246    fn has_more(&self) -> bool {
247        self.has_more
248    }
249}
250
251impl<T: Paginate + DeserializeOwned + Send + Sync + 'static + Clone + std::fmt::Debug> PaginableList
252    for List<T>
253{
254    type O = T;
255
256    fn new(data: Vec<Self::O>, url: String, has_more: bool, total_count: Option<u64>) -> List<T> {
257        Self { url, has_more, data, total_count }
258    }
259
260    fn get_data_mut(&mut self) -> &mut Vec<Self::O> {
261        &mut self.data
262    }
263
264    fn get_data(&self) -> &Vec<Self::O> {
265        &self.data
266    }
267
268    fn get_url(&self) -> String {
269        self.url.clone()
270    }
271    fn get_total_count(&self) -> Option<u64> {
272        self.total_count
273    }
274    fn has_more(&self) -> bool {
275        self.has_more
276    }
277}
278
279impl<T> SearchList<T> {
280    pub fn paginate<P>(self, params: P) -> ListPaginator<SearchList<T>, P> {
281        ListPaginator { page: self, params }
282    }
283}
284
285/// A single page of a cursor-paginated list of an object.
286///
287/// For more details, see <https://stripe.com/docs/api/pagination>
288#[derive(Debug, Clone, Deserialize, Serialize)]
289pub struct List<T> {
290    pub data: Vec<T>,
291    pub has_more: bool,
292    pub total_count: Option<u64>,
293    pub url: String,
294}
295
296impl<T> Default for List<T> {
297    fn default() -> Self {
298        List { data: Vec::new(), has_more: false, total_count: None, url: String::new() }
299    }
300}
301
302impl<T> List<T> {
303    pub fn paginate<P>(self, params: P) -> ListPaginator<List<T>, P> {
304        ListPaginator { page: self, params }
305    }
306}
307
308#[derive(Debug)]
309pub struct ListPaginator<T, P> {
310    pub page: T,
311    pub params: P,
312}
313
314impl<
315        T: PaginableList + Send + DeserializeOwned + 'static,
316        P: Clone + Serialize + Send + 'static + std::fmt::Debug,
317    > ListPaginator<T, P>
318where
319    P: Paginable<O = T::O>,
320{
321    /// Repeatedly queries Stripe for more data until all elements in list are fetched, using
322    /// Stripe's default page size.
323    ///
324    /// Requires `feature = "blocking"`.
325    #[cfg(feature = "blocking")]
326    pub fn get_all(self, client: &Client) -> Response<Vec<T::O>> {
327        let mut data = Vec::with_capacity(self.page.get_total_count().unwrap_or(0) as usize);
328        let mut paginator = self;
329        loop {
330            if !paginator.page.has_more() {
331                data.append(paginator.page.get_data_mut());
332                break;
333            }
334            let next_paginator = paginator.next(client)?;
335            data.append(paginator.page.get_data_mut());
336            paginator = next_paginator
337        }
338        Ok(data)
339    }
340
341    /// Get all values in this List, consuming self and lazily paginating until all values are fetched.
342    ///
343    /// This function repeatedly queries Stripe for more data until all elements in list are fetched, using
344    /// the page size specified in params, or Stripe's default page size if none is specified.
345    ///
346    /// ```no_run
347    /// # use stripe::{Customer, ListCustomers, StripeError, Client};
348    /// # use futures_util::TryStreamExt;
349    /// # async fn run() -> Result<(), StripeError> {
350    /// # let client = Client::new("sk_test_123");
351    /// # let params = ListCustomers { ..Default::default() };
352    ///
353    /// let list = Customer::list(&client, &params).await.unwrap().paginate(params);
354    /// let mut stream = list.stream(&client);
355    ///
356    /// // take a value out from the stream
357    /// if let Some(val) = stream.try_next().await? {
358    ///     println!("GOT = {:?}", val);
359    /// }
360    ///
361    /// // alternatively, you can use stream combinators
362    /// let all_values = stream.try_collect::<Vec<_>>().await?;
363    ///
364    /// # Ok(())
365    /// # }
366    /// ```
367    ///
368    /// Requires `feature = ["async", "stream"]`.
369    #[cfg(all(feature = "async", feature = "stream"))]
370    pub fn stream(
371        mut self,
372        client: &Client,
373    ) -> impl futures_util::Stream<Item = Result<T::O, StripeError>> + Unpin {
374        // We are going to be popping items off the end of the list, so we need to reverse it.
375        self.page.get_data_mut().reverse();
376
377        Box::pin(futures_util::stream::unfold(Some((self, client.clone())), Self::unfold_stream))
378    }
379
380    /// unfold a single item from the stream
381    #[cfg(all(feature = "async", feature = "stream"))]
382    async fn unfold_stream(
383        state: Option<(Self, Client)>,
384    ) -> Option<(Result<T::O, StripeError>, Option<(Self, Client)>)> {
385        let (mut paginator, client) = state?; // If none, we sent the last item in the last iteration
386
387        if paginator.page.get_data().len() > 1 {
388            return Some((Ok(paginator.page.get_data_mut().pop()?), Some((paginator, client))));
389            // We have more data on this page
390        }
391
392        if !paginator.page.has_more() {
393            return Some((Ok(paginator.page.get_data_mut().pop()?), None)); // Final value of the stream, no errors
394        }
395
396        match paginator.next(&client).await {
397            Ok(mut next_paginator) => {
398                let data = paginator.page.get_data_mut().pop()?;
399                next_paginator.page.get_data_mut().reverse();
400
401                // Yield last value of thimuts page, the next page (and client) becomes the state
402                Some((Ok(data), Some((next_paginator, client))))
403            }
404            Err(e) => Some((Err(e), None)), // We ran into an error. The last value of the stream will be the error.
405        }
406    }
407
408    /// Fetch an additional page of data from stripe.
409    pub fn next(&self, client: &Client) -> Response<Self> {
410        if let Some(last) = self.page.get_data().last() {
411            if self.page.get_url().starts_with("/v1/") {
412                let path = self.page.get_url().trim_start_matches("/v1/").to_string(); // the url we get back is prefixed
413
414                // clone the params and set the cursor
415                let params_next = {
416                    let mut p = self.params.clone();
417                    p.set_last(last.clone());
418                    p
419                };
420
421                let page = client.get_query(&path, &params_next);
422
423                ListPaginator::create_paginator(page, params_next)
424            } else {
425                err(StripeError::UnsupportedVersion)
426            }
427        } else {
428            ok(ListPaginator {
429                page: T::new(Vec::new(), self.page.get_url(), false, self.page.get_total_count()),
430                params: self.params.clone(),
431            })
432        }
433    }
434
435    /// Pin a new future which maps the result inside the page future into
436    /// a ListPaginator
437    #[cfg(feature = "async")]
438    fn create_paginator(page: Response<T>, params: P) -> Response<Self> {
439        use futures_util::FutureExt;
440        Box::pin(page.map(|page| page.map(|page| ListPaginator { page, params })))
441    }
442
443    #[cfg(feature = "blocking")]
444    fn create_paginator(page: Response<T>, params: P) -> Response<Self> {
445        page.map(|page| ListPaginator { page, params })
446    }
447}
448
449pub type CurrencyMap<V> = HashMap<Currency, V>;
450pub type Metadata = HashMap<String, String>;
451pub type Timestamp = i64;
452
453#[derive(Clone, Debug, Deserialize, Serialize)]
454#[serde(rename_all = "lowercase")]
455pub struct RangeBounds<T> {
456    pub gt: Option<T>,
457    pub gte: Option<T>,
458    pub lt: Option<T>,
459    pub lte: Option<T>,
460}
461
462impl<T> Default for RangeBounds<T> {
463    fn default() -> Self {
464        RangeBounds { gt: None, gte: None, lt: None, lte: None }
465    }
466}
467
468/// A set of generic request parameters that can be used on
469/// list endpoints to filter their results by some timestamp.
470#[derive(Clone, Debug, Deserialize, Serialize)]
471#[serde(untagged)]
472pub enum RangeQuery<T> {
473    Exact(T),
474    Bounds(RangeBounds<T>),
475}
476
477impl<T> RangeQuery<T> {
478    /// Filter results to exactly match a given value
479    pub fn eq(value: T) -> RangeQuery<T> {
480        RangeQuery::Exact(value)
481    }
482
483    /// Filter results to be after a given value
484    pub fn gt(value: T) -> RangeQuery<T> {
485        RangeQuery::Bounds(RangeBounds { gt: Some(value), ..Default::default() })
486    }
487
488    /// Filter results to be after or equal to a given value
489    pub fn gte(value: T) -> RangeQuery<T> {
490        RangeQuery::Bounds(RangeBounds { gte: Some(value), ..Default::default() })
491    }
492
493    /// Filter results to be before to a given value
494    pub fn lt(value: T) -> RangeQuery<T> {
495        RangeQuery::Bounds(RangeBounds { lt: Some(value), ..Default::default() })
496    }
497
498    /// Filter results to be before or equal to a given value
499    pub fn lte(value: T) -> RangeQuery<T> {
500        RangeQuery::Bounds(RangeBounds { lte: Some(value), ..Default::default() })
501    }
502}
503
504#[derive(Clone, Debug, Serialize)]
505#[serde(untagged)]
506pub enum IdOrCreate<'a, T> {
507    Id(&'a str),
508    Create(&'a T),
509}
510
511// NOTE: Only intended to handle conversion from ASCII CamelCase to SnakeCase
512//   This function is used to convert static Rust identifiers to snakecase
513// TODO: pub(crate) fn
514pub fn to_snakecase(camel: &str) -> String {
515    let mut i = 0;
516    let mut snake = String::new();
517    let mut chars = camel.chars().peekable();
518    while let Some(ch) = chars.next() {
519        if ch.is_uppercase() {
520            if i > 0 && !chars.peek().unwrap_or(&'A').is_uppercase() {
521                snake.push('_');
522            }
523            snake.push(ch.to_lowercase().next().unwrap_or(ch));
524        } else {
525            snake.push(ch);
526        }
527        i += 1;
528    }
529
530    snake
531}
532
533#[cfg(test)]
534mod tests {
535    #[test]
536    fn to_snakecase() {
537        use super::to_snakecase;
538
539        assert_eq!(to_snakecase("snake_case").as_str(), "snake_case");
540        assert_eq!(to_snakecase("CamelCase").as_str(), "camel_case");
541        assert_eq!(to_snakecase("XMLHttpRequest").as_str(), "xml_http_request");
542        assert_eq!(to_snakecase("UPPER").as_str(), "upper");
543        assert_eq!(to_snakecase("lower").as_str(), "lower");
544    }
545
546    #[cfg(feature = "async")]
547    #[tokio::test]
548    async fn list() {
549        use httpmock::Method::GET;
550        use httpmock::MockServer;
551
552        use crate::Client;
553        use crate::{Customer, ListCustomers};
554
555        // Start a lightweight mock server.
556        let server = MockServer::start_async().await;
557
558        let client = Client::from_url(&*server.url("/"), "fake_key");
559
560        let next_item = server.mock(|when, then| {
561            when.method(GET).path("/v1/customers").query_param("starting_after", "cus_1");
562            then.status(200).body(
563                r#"{"object": "list", "data": [{
564                "id": "cus_2",
565                "object": "customer",
566                "balance": 0,
567                "created": 1649316731,
568                "currency": "gbp",
569                "delinquent": false,
570                "email": null,
571                "invoice_prefix": "4AF7482",
572                "invoice_settings": {},
573                "livemode": false,
574                "metadata": {},
575                "preferred_locales": [],
576                "tax_exempt": "none"
577              }], "has_more": false, "url": "/v1/customers"}"#,
578            );
579        });
580
581        let first_item = server.mock(|when, then| {
582            when.method(GET).path("/v1/customers");
583            then.status(200).body(
584                r#"{"object": "list", "data": [{
585                "id": "cus_1",
586                "object": "customer",
587                "balance": 0,
588                "created": 1649316731,
589                "currency": "gbp",
590                "delinquent": false,
591                "invoice_prefix": "4AF7482",
592                "invoice_settings": {},
593                "livemode": false,
594                "metadata": {},
595                "preferred_locales": [],
596                "tax_exempt": "none"
597              }], "has_more": true, "url": "/v1/customers"}"#,
598            );
599        });
600
601        let params = ListCustomers::new();
602        let res = Customer::list(&client, &params).await.unwrap().paginate(params);
603
604        println!("{:?}", res);
605
606        let res2 = res.next(&client).await.unwrap();
607
608        println!("{:?}", res2);
609
610        first_item.assert_hits_async(1).await;
611        next_item.assert_hits_async(1).await;
612    }
613
614    #[cfg(feature = "blocking")]
615    #[test]
616    fn get_all() {
617        use httpmock::Method::GET;
618        use httpmock::MockServer;
619
620        use crate::Client;
621        use crate::{Customer, ListCustomers};
622
623        // Start a lightweight mock server.
624        let server = MockServer::start();
625
626        let client = Client::from_url(&*server.url("/"), "fake_key");
627
628        let next_item = server.mock(|when, then| {
629            when.method(GET).path("/v1/customers").query_param("starting_after", "cus_2");
630            then.status(200).body(
631                r#"{"object": "list", "data": [{
632                "id": "cus_2",
633                "object": "customer",
634                "balance": 0,
635                "created": 1649316733,
636                "currency": "gbp",
637                "delinquent": false,
638                "email": null,
639                "invoice_prefix": "4AF7482",
640                "invoice_settings": {},
641                "livemode": false,
642                "metadata": {},
643                "preferred_locales": [],
644                "tax_exempt": "none"
645              }], "has_more": false, "url": "/v1/customers"}"#,
646            );
647        });
648
649        let first_item = server.mock(|when, then| {
650            when.method(GET).path("/v1/customers");
651            then.status(200).body(
652                r#"{"object": "list", "data": [{
653                "id": "cus_1",
654                "object": "customer",
655                "balance": 0,
656                "created": 1649316732,
657                "currency": "gbp",
658                "delinquent": false,
659                "invoice_prefix": "4AF7482",
660                "invoice_settings": {},
661                "livemode": false,
662                "metadata": {},
663                "preferred_locales": [],
664                "tax_exempt": "none"
665              }, {
666                "id": "cus_2",
667                "object": "customer",
668                "balance": 0,
669                "created": 1649316733,
670                "currency": "gbp",
671                "delinquent": false,
672                "invoice_prefix": "4AF7482",
673                "invoice_settings": {},
674                "livemode": false,
675                "metadata": {},
676                "preferred_locales": [],
677                "tax_exempt": "none"
678              }], "has_more": true, "url": "/v1/customers"}"#,
679            );
680        });
681
682        let params = ListCustomers::new();
683        let res = Customer::list(&client, &params).unwrap().paginate(params);
684
685        let customers = res.get_all(&client).unwrap();
686
687        println!("{:?}", customers);
688
689        assert_eq!(customers.len(), 3);
690        first_item.assert_hits(1);
691        next_item.assert_hits(1);
692    }
693
694    #[cfg(feature = "async")]
695    #[tokio::test]
696    async fn list_multiple() {
697        use httpmock::Method::GET;
698        use httpmock::MockServer;
699
700        use crate::Client;
701        use crate::{Customer, ListCustomers};
702
703        // Start a lightweight mock server.
704        let server = MockServer::start_async().await;
705
706        let client = Client::from_url(&*server.url("/"), "fake_key");
707
708        let next_item = server.mock(|when, then| {
709            when.method(GET).path("/v1/customers").query_param("starting_after", "cus_2");
710            then.status(200).body(
711                r#"{"object": "list", "data": [{
712                "id": "cus_2",
713                "object": "customer",
714                "balance": 0,
715                "created": 1649316733,
716                "currency": "gbp",
717                "delinquent": false,
718                "email": null,
719                "invoice_prefix": "4AF7482",
720                "invoice_settings": {},
721                "livemode": false,
722                "metadata": {},
723                "preferred_locales": [],
724                "tax_exempt": "none"
725              }], "has_more": false, "url": "/v1/customers"}"#,
726            );
727        });
728
729        let first_item = server.mock(|when, then| {
730            when.method(GET).path("/v1/customers");
731            then.status(200).body(
732                r#"{"object": "list", "data": [{
733                "id": "cus_1",
734                "object": "customer",
735                "balance": 0,
736                "created": 1649316732,
737                "currency": "gbp",
738                "delinquent": false,
739                "invoice_prefix": "4AF7482",
740                "invoice_settings": {},
741                "livemode": false,
742                "metadata": {},
743                "preferred_locales": [],
744                "tax_exempt": "none"
745              }, {
746                "id": "cus_2",
747                "object": "customer",
748                "balance": 0,
749                "created": 1649316733,
750                "currency": "gbp",
751                "delinquent": false,
752                "invoice_prefix": "4AF7482",
753                "invoice_settings": {},
754                "livemode": false,
755                "metadata": {},
756                "preferred_locales": [],
757                "tax_exempt": "none"
758              }], "has_more": true, "url": "/v1/customers"}"#,
759            );
760        });
761
762        let params = ListCustomers::new();
763        let res = Customer::list(&client, &params).await.unwrap().paginate(params);
764
765        let res2 = res.next(&client).await.unwrap();
766
767        println!("{:?}", res2);
768
769        first_item.assert_hits_async(1).await;
770        next_item.assert_hits_async(1).await;
771    }
772
773    #[cfg(all(feature = "async", feature = "stream"))]
774    #[tokio::test]
775    async fn stream() {
776        use futures_util::StreamExt;
777        use httpmock::Method::GET;
778        use httpmock::MockServer;
779
780        use crate::Client;
781        use crate::{Customer, ListCustomers};
782
783        // Start a lightweight mock server.
784        let server = MockServer::start_async().await;
785
786        let client = Client::from_url(&*server.url("/"), "fake_key");
787
788        let next_item = server.mock(|when, then| {
789            when.method(GET).path("/v1/customers").query_param("starting_after", "cus_1");
790            then.status(200).body(
791                r#"{"object": "list", "data": [{
792                "id": "cus_2",
793                "object": "customer",
794                "balance": 0,
795                "created": 1649316731,
796                "currency": "gbp",
797                "delinquent": false,
798                "email": null,
799                "invoice_prefix": "4AF7482",
800                "invoice_settings": {},
801                "livemode": false,
802                "metadata": {},
803                "preferred_locales": [],
804                "tax_exempt": "none"
805              }], "has_more": false, "url": "/v1/customers"}"#,
806            );
807        });
808
809        let first_item = server.mock(|when, then| {
810            when.method(GET).path("/v1/customers");
811            then.status(200).body(
812                r#"{"object": "list", "data": [{
813                "id": "cus_1",
814                "object": "customer",
815                "balance": 0,
816                "created": 1649316731,
817                "currency": "gbp",
818                "delinquent": false,
819                "invoice_prefix": "4AF7482",
820                "invoice_settings": {},
821                "livemode": false,
822                "metadata": {},
823                "preferred_locales": [],
824                "tax_exempt": "none"
825              }], "has_more": true, "url": "/v1/customers"}"#,
826            );
827        });
828
829        let params = ListCustomers::new();
830        let res = Customer::list(&client, &params).await.unwrap().paginate(params);
831
832        let stream = res.stream(&client).collect::<Vec<_>>().await;
833
834        println!("{:#?}", stream);
835        assert_eq!(stream.len(), 2);
836
837        first_item.assert_hits_async(1).await;
838        next_item.assert_hits_async(1).await;
839    }
840
841    #[cfg(all(feature = "async", feature = "stream"))]
842    #[tokio::test]
843    async fn stream_multiple() {
844        use futures_util::StreamExt;
845        use httpmock::Method::GET;
846        use httpmock::MockServer;
847
848        use crate::Client;
849        use crate::{Customer, ListCustomers};
850
851        // Start a lightweight mock server.
852        let server = MockServer::start_async().await;
853
854        let client = Client::from_url(&*server.url("/"), "fake_key");
855
856        let next_item = server.mock(|when, then| {
857            when.method(GET).path("/v1/customers").query_param("starting_after", "cus_2");
858            then.status(200).body(
859                r#"{"object": "list", "data": [{
860                "id": "cus_3",
861                "object": "customer",
862                "balance": 0,
863                "created": 1649316734,
864                "currency": "gbp",
865                "delinquent": false,
866                "email": null,
867                "invoice_prefix": "4AF7482",
868                "invoice_settings": {},
869                "livemode": false,
870                "metadata": {},
871                "preferred_locales": [],
872                "tax_exempt": "none"
873              }], "has_more": false, "url": "/v1/customers"}"#,
874            );
875        });
876
877        let items = server.mock(|when, then| {
878            when.method(GET).path("/v1/customers");
879            then.status(200).body(
880                r#"{"object": "list", "data": [{
881                "id": "cus_1",
882                "object": "customer",
883                "balance": 0,
884                "created": 1649316732,
885                "currency": "gbp",
886                "delinquent": false,
887                "invoice_prefix": "4AF7482",
888                "invoice_settings": {},
889                "livemode": false,
890                "metadata": {},
891                "preferred_locales": [],
892                "tax_exempt": "none"
893              }, {
894                "id": "cus_2",
895                "object": "customer",
896                "balance": 0,
897                "created": 1649316733,
898                "currency": "gbp",
899                "delinquent": false,
900                "invoice_prefix": "4AF7482",
901                "invoice_settings": {},
902                "livemode": false,
903                "metadata": {},
904                "preferred_locales": [],
905                "tax_exempt": "none"
906              }], "has_more": true, "url": "/v1/customers"}"#,
907            );
908        });
909
910        let params = ListCustomers::default();
911        let res = Customer::list(&client, &params).await.unwrap().paginate(params);
912
913        let stream = res.stream(&client).collect::<Vec<_>>().await;
914
915        println!("{:#?}", stream.len());
916        assert_eq!(stream.len(), 3);
917
918        items.assert_hits_async(1).await;
919        next_item.assert_hits_async(1).await;
920    }
921}