Skip to main content

google_cloud_gax/
paginator.rs

1// Copyright 2024 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Defines some types and traits to simplify working with list RPCs.
16//!
17//! When listing large collections of items Google Cloud services typically
18//! break the responses in "pages". Each one of these pages contains a limited
19//! number of items, and a token to request the next page. The caller must
20//! repeatedly call the list RPC (using the token from the previous page) to
21//! obtain additional items.
22//!
23//! Sequencing these calls can be tedious. The Google Cloud client libraries
24//! for Rust provide implementations of this trait to simplify the use of
25//! these paginated APIs.
26//!
27//! For more information on the RPCs with paginated responses see [AIP-4233].
28//!
29//! # Example: stream each item for a list operation
30//! ```no_run
31//! # use google_cloud_gax::{paginator, Result, error::Error};
32//! struct Page { items: Vec<String>, token: String }
33//! # impl paginator::internal::PageableResponse for Page {
34//! #     type PageItem = String;
35//! #     fn items(self) -> Vec<String> { self.items }
36//! #     fn next_page_token(&self) -> String { self.token.clone() }
37//! # }
38//! # async fn get_page(_: String) -> Result<Page> { panic!(); }
39//! use paginator::{Paginator, ItemPaginator};
40//! fn list() -> impl Paginator<Page, Error> {
41//!     // ... details omitted ...
42//!     # fn execute(_: String) -> Result<Page> { panic!(); }
43//!     # paginator::internal::new_paginator(String::new(), get_page)
44//! }
45//! # async fn sample() -> anyhow::Result<()> {
46//! let mut items = list().items();
47//! while let Some(item) = items.next().await {
48//!     let item = item?;
49//!     println!("  item = {item}");
50//! }
51//! # Ok(()) }
52//! ```
53//!
54//! # Example: manually iterate over all the pages for a list operation
55//! ```no_run
56//! # use google_cloud_gax::{paginator, Result, error::Error};
57//! struct Page { items: Vec<String>, token: String }
58//! # impl paginator::internal::PageableResponse for Page {
59//! #     type PageItem = String;
60//! #     fn items(self) -> Vec<String> { self.items }
61//! #     fn next_page_token(&self) -> String { self.token.clone() }
62//! # }
63//! # async fn get_page(_: String) -> Result<Page> { panic!(); }
64//! use paginator::Paginator;
65//! fn list() -> impl Paginator<Page, Error> {
66//!     // ... details omitted ...
67//!     # fn execute(_: String) -> Result<Page> { panic!(); }
68//!     # paginator::internal::new_paginator(String::new(), get_page)
69//! }
70//! # async fn sample() -> anyhow::Result<()> {
71//! let mut pages = list();
72//! while let Some(page) = pages.next().await {
73//!     let page = page?;
74//!     println!("a page with {} items", page.items.len());
75//!     page.items.into_iter().enumerate().for_each(|(n, i)| println!("  item[{n}]= {i}"));
76//! }
77//! # Ok(()) }
78//! ```
79//!
80//! [AIP-4233]: https://google.aip.dev/client-libraries/4233
81
82use futures::stream::unfold;
83use futures::{Stream, StreamExt};
84use pin_project::pin_project;
85use std::future::Future;
86use std::pin::Pin;
87
88#[cfg_attr(not(feature = "_internal-semver"), doc(hidden))]
89#[allow(missing_docs)]
90pub mod internal {
91    //! This module contains implementation details. It is not part of the
92    //! public API. Types and functions in this module may be changed or removed
93    //! without warnings. Applications should not use any types contained
94    //! within.
95    use super::*;
96
97    /// Describes a type that can be iterated over asynchronously when used with
98    /// [super::Paginator].
99    pub trait PageableResponse {
100        type PageItem: Send;
101
102        // Consumes the [PageableResponse] and returns the items associated with the
103        // current page.
104        fn items(self) -> Vec<Self::PageItem>;
105
106        /// Returns the next page token.
107        fn next_page_token(&self) -> String;
108    }
109
110    /// Creates a new `impl Paginator<T, E>` given the initial page token and a function
111    /// to fetch the next response.
112    pub fn new_paginator<T, E, F>(
113        seed_token: String,
114        execute: impl Fn(String) -> F + Clone + Send + 'static,
115    ) -> impl Paginator<T, E>
116    where
117        T: internal::PageableResponse,
118        F: Future<Output = Result<T, E>> + Send + 'static,
119    {
120        PaginatorImpl::new(seed_token, execute)
121    }
122}
123
124mod sealed {
125    pub trait Paginator {}
126}
127
128/// Automatically streams pages in list RPCs.
129///
130/// When listing large collections of items Google Cloud services typically
131/// break the responses in "pages", where each page contains a limited number
132/// of items, and a token to request the next page. The caller must
133/// repeatedly call the list RPC (using the token from the previous page) to
134/// obtain additional items.
135///
136/// Sequencing these calls can be tedious. The Google Cloud client libraries
137/// for Rust provide implementations of this trait to simplify the use of
138/// these paginated APIs.
139///
140/// Enable the `unstable-stream` feature to convert this type to a [Stream].
141///
142/// For more information on the RPCs with paginated responses see [AIP-4233].
143///
144/// [AIP-4233]: https://google.aip.dev/client-libraries/4233
145pub trait Paginator<PageType, Error>: Send + sealed::Paginator
146where
147    PageType: internal::PageableResponse,
148{
149    /// Creates a new [ItemPaginator] from an existing [Paginator].
150    fn items(self) -> impl ItemPaginator<PageType, Error>;
151
152    /// Returns the next mutation of the wrapped stream.
153    fn next(&mut self) -> impl Future<Output = Option<Result<PageType, Error>>> + Send;
154
155    #[cfg(feature = "unstable-stream")]
156    #[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
157    /// Convert the paginator to a [Stream].
158    fn into_stream(self) -> impl futures::Stream<Item = Result<PageType, Error>> + Unpin;
159}
160
161#[pin_project]
162struct PaginatorImpl<T, E> {
163    #[pin]
164    stream: Pin<Box<dyn Stream<Item = Result<T, E>> + Send>>,
165}
166
167type ControlFlow = std::ops::ControlFlow<(), String>;
168
169impl<T, E> PaginatorImpl<T, E>
170where
171    T: internal::PageableResponse,
172{
173    /// Creates a new [Paginator] given the initial page token and a function
174    /// to fetch the next response.
175    pub fn new<F>(
176        seed_token: String,
177        execute: impl Fn(String) -> F + Clone + Send + 'static,
178    ) -> Self
179    where
180        F: Future<Output = Result<T, E>> + Send + 'static,
181    {
182        let stream = unfold(ControlFlow::Continue(seed_token), move |state| {
183            let execute = execute.clone();
184            async move {
185                let token = match state {
186                    ControlFlow::Continue(token) => token,
187                    ControlFlow::Break(_) => return None,
188                };
189                match execute(token).await {
190                    Ok(page_resp) => {
191                        let tok = page_resp.next_page_token();
192                        let next_state = if tok.is_empty() {
193                            ControlFlow::Break(())
194                        } else {
195                            ControlFlow::Continue(tok)
196                        };
197                        Some((Ok(page_resp), next_state))
198                    }
199                    Err(e) => Some((Err(e), ControlFlow::Break(()))),
200                }
201            }
202        });
203        Self {
204            stream: Box::pin(stream),
205        }
206    }
207}
208
209impl<T, E> Paginator<T, E> for PaginatorImpl<T, E>
210where
211    T: internal::PageableResponse,
212{
213    /// Creates a new [ItemPaginator] from an existing [Paginator].
214    fn items(self) -> impl ItemPaginator<T, E> {
215        ItemPaginatorImpl::new(self)
216    }
217
218    /// Returns the next mutation of the wrapped stream.
219    async fn next(&mut self) -> Option<Result<T, E>> {
220        self.stream.next().await
221    }
222
223    #[cfg(feature = "unstable-stream")]
224    #[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
225    /// Convert the paginator to a [Stream][futures::stream::Stream].
226    fn into_stream(self) -> impl futures::Stream<Item = Result<T, E>> + Unpin {
227        Box::pin(unfold(self, |mut paginator| async move {
228            paginator.next().await.map(|item| (item, paginator))
229        }))
230    }
231}
232
233impl<T, E> sealed::Paginator for PaginatorImpl<T, E> where T: internal::PageableResponse {}
234
235impl<T, E> std::fmt::Debug for PaginatorImpl<T, E> {
236    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
237        f.debug_struct("Paginator").finish()
238    }
239}
240
241/// Automatically stream items in list RPCs.
242///
243/// When listing large collections of items Google Cloud services typically
244/// break the responses in "pages", where each page contains a subset of
245/// the items, and a token to request the next page. The caller must
246/// repeatedly call the list RPC using the token from the previous page.
247///
248/// Sequencing these calls can be tedious. The Google Cloud client libraries
249/// for rust provide implementations of this trait to simplify the use of
250/// these paginated APIs.
251///
252/// Many applications want to operate on one item at a time. The Google Cloud
253/// client libraries for Rust provide implementations of this trait that
254/// automatically iterate over each item and then fetch the next page, stopping
255/// when all the pages are processed.
256///
257/// Enable the `unstable-stream` feature to convert this type to a [Stream].
258///
259/// For more information on the RPCs with paginated responses see [AIP-4233].
260///
261/// [AIP-4233]: https://google.aip.dev/client-libraries/4233
262pub trait ItemPaginator<PageType, Error>: Send + sealed::Paginator
263where
264    PageType: internal::PageableResponse,
265{
266    /// Returns the next item from the stream of (paginated) responses.
267    fn next(&mut self) -> impl Future<Output = Option<Result<PageType::PageItem, Error>>> + Send;
268
269    #[cfg(feature = "unstable-stream")]
270    #[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
271    /// Convert the paginator to a [Stream].
272    fn into_stream(self) -> impl futures::Stream<Item = Result<PageType::PageItem, Error>> + Unpin;
273}
274
275#[pin_project]
276struct ItemPaginatorImpl<T, E>
277where
278    T: internal::PageableResponse,
279{
280    #[pin]
281    stream: PaginatorImpl<T, E>,
282    current_items: Option<std::vec::IntoIter<T::PageItem>>,
283}
284
285impl<T, E> ItemPaginatorImpl<T, E>
286where
287    T: internal::PageableResponse,
288{
289    /// Creates a new [ItemPaginator] from an existing [Paginator].
290    fn new(paginator: PaginatorImpl<T, E>) -> Self {
291        Self {
292            stream: paginator,
293            current_items: None,
294        }
295    }
296}
297
298impl<T, E> ItemPaginator<T, E> for ItemPaginatorImpl<T, E>
299where
300    T: internal::PageableResponse,
301{
302    /// Returns the next item in the stream of (paginated) responses.
303    ///
304    /// Enable the `unstable-stream` feature to convert this to a [Stream].
305    async fn next(&mut self) -> Option<Result<T::PageItem, E>> {
306        loop {
307            if let Some(ref mut iter) = self.current_items
308                && let Some(item) = iter.next()
309            {
310                return Some(Ok(item));
311            }
312
313            let next_page = self.stream.next().await;
314            match next_page {
315                Some(Ok(page)) => {
316                    self.current_items = Some(page.items().into_iter());
317                }
318                Some(Err(e)) => {
319                    return Some(Err(e));
320                }
321                None => return None,
322            }
323        }
324    }
325
326    #[cfg(feature = "unstable-stream")]
327    #[cfg_attr(docsrs, doc(cfg(feature = "unstable-stream")))]
328    /// Convert the paginator to a [Stream].
329    fn into_stream(self) -> impl Stream<Item = Result<T::PageItem, E>> + Unpin {
330        Box::pin(unfold(self, |mut paginator| async move {
331            paginator.next().await.map(|item| (item, paginator))
332        }))
333    }
334}
335
336impl<T, E> sealed::Paginator for ItemPaginatorImpl<T, E> where T: internal::PageableResponse {}
337
338#[cfg(test)]
339mod tests {
340    use super::internal::*;
341    use super::*;
342    use std::collections::VecDeque;
343    use std::sync::{Arc, Mutex};
344
345    #[derive(Clone, Default)]
346    struct TestRequest {
347        page_token: String,
348    }
349
350    struct TestResponse {
351        items: Vec<PageItem>,
352        next_page_token: String,
353    }
354
355    #[derive(Clone)]
356    struct PageItem {
357        name: String,
358    }
359
360    impl PageableResponse for TestResponse {
361        type PageItem = PageItem;
362
363        fn items(self) -> Vec<Self::PageItem> {
364            self.items
365        }
366
367        fn next_page_token(&self) -> String {
368            self.next_page_token.clone()
369        }
370    }
371
372    #[derive(Clone)]
373    struct Client {
374        inner: Arc<InnerClient>,
375    }
376
377    struct InnerClient {
378        data: Arc<Mutex<Vec<TestResponse>>>,
379    }
380
381    impl Client {
382        async fn execute(
383            data: Arc<Mutex<Vec<TestResponse>>>,
384            _: TestRequest,
385        ) -> Result<TestResponse, Box<dyn std::error::Error>> {
386            // This is where we could run a request with a client
387            let mut responses = data.lock().unwrap();
388            let resp: TestResponse = responses.remove(0);
389            Ok(resp)
390        }
391
392        async fn list_rpc(
393            &self,
394            req: TestRequest,
395        ) -> Result<TestResponse, Box<dyn std::error::Error>> {
396            let inner = self.inner.clone();
397            Client::execute(inner.data.clone(), req).await
398        }
399
400        fn list_rpc_stream(
401            &self,
402            req: TestRequest,
403        ) -> impl Paginator<TestResponse, Box<dyn std::error::Error>> {
404            let client = self.clone();
405            let tok = req.page_token.clone();
406            let execute = move |token| {
407                let mut req = req.clone();
408                let client = client.clone();
409                req.page_token = token;
410                async move { client.list_rpc(req).await }
411            };
412            new_paginator(tok, execute)
413        }
414    }
415
416    #[tokio::test]
417    async fn test_pagination() {
418        let seed = "token1".to_string();
419        let mut responses = VecDeque::new();
420        responses.push_back(TestResponse {
421            items: vec![
422                PageItem {
423                    name: "item1".to_string(),
424                },
425                PageItem {
426                    name: "item2".to_string(),
427                },
428            ],
429            next_page_token: "token2".to_string(),
430        });
431        responses.push_back(TestResponse {
432            items: vec![PageItem {
433                name: "item3".to_string(),
434            }],
435            next_page_token: "".to_string(),
436        });
437        let mut expected_tokens = VecDeque::new();
438        expected_tokens.push_back("token1".to_string());
439        expected_tokens.push_back("token2".to_string());
440
441        let state = Arc::new(Mutex::new(responses));
442        let tokens = Arc::new(Mutex::new(expected_tokens));
443
444        let execute = move |token: String| {
445            let expected_token = tokens.clone().lock().unwrap().pop_front().unwrap();
446            assert_eq!(token, expected_token);
447            let resp: TestResponse = state.clone().lock().unwrap().pop_front().unwrap();
448            async move { Ok::<_, Box<dyn std::error::Error>>(resp) }
449        };
450
451        let mut resps = vec![];
452        let mut paginator = new_paginator(seed, execute);
453        while let Some(resp) = paginator.next().await {
454            if let Ok(resp) = resp {
455                resps.push(resp)
456            }
457        }
458        assert_eq!(resps.len(), 2);
459        assert_eq!(resps[0].items[0].name, "item1");
460        assert_eq!(resps[0].items[1].name, "item2");
461    }
462
463    #[tokio::test]
464    async fn test_paginator_as_client() {
465        let responses = vec![
466            TestResponse {
467                items: vec![
468                    PageItem {
469                        name: "item1".to_string(),
470                    },
471                    PageItem {
472                        name: "item2".to_string(),
473                    },
474                ],
475                next_page_token: "token1".to_string(),
476            },
477            TestResponse {
478                items: vec![PageItem {
479                    name: "item3".to_string(),
480                }],
481                next_page_token: "".to_string(),
482            },
483        ];
484
485        let client = Client {
486            inner: Arc::new(InnerClient {
487                data: Arc::new(Mutex::new(responses)),
488            }),
489        };
490        let mut resps = vec![];
491        let mut paginator = client.list_rpc_stream(TestRequest::default());
492        while let Some(resp) = paginator.next().await {
493            if let Ok(resp) = resp {
494                resps.push(resp)
495            }
496        }
497        assert_eq!(resps.len(), 2);
498        assert_eq!(resps[0].items[0].name, "item1");
499        assert_eq!(resps[0].items[1].name, "item2");
500        assert_eq!(resps[1].items[0].name, "item3");
501    }
502
503    #[tokio::test]
504    async fn test_paginator_error() {
505        let execute = |_| async { Err::<TestResponse, Box<dyn std::error::Error>>("err".into()) };
506
507        let mut paginator = new_paginator(String::new(), execute);
508        let mut count = 0;
509        while let Some(resp) = paginator.next().await {
510            match resp {
511                Ok(_) => {
512                    panic!("Should not succeed");
513                }
514                Err(e) => {
515                    assert_eq!(e.to_string(), "err");
516                    count += 1;
517                }
518            }
519        }
520        assert_eq!(count, 1);
521    }
522
523    #[cfg(feature = "unstable-stream")]
524    #[tokio::test]
525    async fn test_paginator_into_stream() {
526        let responses = vec![
527            TestResponse {
528                items: vec![
529                    PageItem {
530                        name: "item1".to_string(),
531                    },
532                    PageItem {
533                        name: "item2".to_string(),
534                    },
535                ],
536                next_page_token: "token1".to_string(),
537            },
538            TestResponse {
539                items: vec![PageItem {
540                    name: "item3".to_string(),
541                }],
542                next_page_token: "".to_string(),
543            },
544        ];
545
546        let client = Client {
547            inner: Arc::new(InnerClient {
548                data: Arc::new(Mutex::new(responses)),
549            }),
550        };
551        let mut resps = vec![];
552        let mut stream = client.list_rpc_stream(TestRequest::default()).into_stream();
553        while let Some(resp) = stream.next().await {
554            if let Ok(resp) = resp {
555                resps.push(resp)
556            }
557        }
558        assert_eq!(resps.len(), 2);
559        assert_eq!(resps[0].items[0].name, "item1");
560        assert_eq!(resps[0].items[1].name, "item2");
561        assert_eq!(resps[1].items[0].name, "item3");
562    }
563
564    #[cfg(feature = "unstable-stream")]
565    #[tokio::test]
566    async fn test_item_paginator_into_stream() {
567        let responses = vec![
568            TestResponse {
569                items: vec![
570                    PageItem {
571                        name: "item1".to_string(),
572                    },
573                    PageItem {
574                        name: "item2".to_string(),
575                    },
576                ],
577                next_page_token: "token1".to_string(),
578            },
579            TestResponse {
580                items: vec![PageItem {
581                    name: "item3".to_string(),
582                }],
583                next_page_token: "".to_string(),
584            },
585        ];
586
587        let client = Client {
588            inner: Arc::new(InnerClient {
589                data: Arc::new(Mutex::new(responses)),
590            }),
591        };
592        let mut items = vec![];
593        let mut stream = client
594            .list_rpc_stream(TestRequest::default())
595            .items()
596            .into_stream();
597        while let Some(item) = stream.next().await {
598            if let Ok(item) = item {
599                items.push(item)
600            }
601        }
602        assert_eq!(items.len(), 3);
603        assert_eq!(items[0].name, "item1");
604        assert_eq!(items[1].name, "item2");
605        assert_eq!(items[2].name, "item3");
606    }
607}