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 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
58pub trait Object {
60 type Id;
62 fn id(&self) -> Self::Id;
64 fn object(&self) -> &'static str;
66}
67
68#[derive(Clone, Debug, Deserialize, Serialize)]
70pub struct Deleted<T> {
71 pub id: T,
73 pub deleted: bool,
75}
76
77#[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#[derive(Clone, Debug, Serialize, Deserialize)] #[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
154pub 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#[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#[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 #[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 #[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 self.page.get_data_mut().reverse();
376
377 Box::pin(futures_util::stream::unfold(Some((self, client.clone())), Self::unfold_stream))
378 }
379
380 #[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 paginator.page.get_data().len() > 1 {
388 return Some((Ok(paginator.page.get_data_mut().pop()?), Some((paginator, client))));
389 }
391
392 if !paginator.page.has_more() {
393 return Some((Ok(paginator.page.get_data_mut().pop()?), None)); }
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 Some((Ok(data), Some((next_paginator, client))))
403 }
404 Err(e) => Some((Err(e), None)), }
406 }
407
408 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(); 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, ¶ms_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 #[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#[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 pub fn eq(value: T) -> RangeQuery<T> {
480 RangeQuery::Exact(value)
481 }
482
483 pub fn gt(value: T) -> RangeQuery<T> {
485 RangeQuery::Bounds(RangeBounds { gt: Some(value), ..Default::default() })
486 }
487
488 pub fn gte(value: T) -> RangeQuery<T> {
490 RangeQuery::Bounds(RangeBounds { gte: Some(value), ..Default::default() })
491 }
492
493 pub fn lt(value: T) -> RangeQuery<T> {
495 RangeQuery::Bounds(RangeBounds { lt: Some(value), ..Default::default() })
496 }
497
498 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
511pub 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 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, ¶ms).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 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, ¶ms).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 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, ¶ms).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 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, ¶ms).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 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, ¶ms).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}