modio/util/
pagination.rs

1use std::fmt;
2use std::marker::PhantomData;
3use std::vec::IntoIter;
4
5use serde::de::DeserializeOwned;
6
7use crate::request::{Filter, RequestBuilder, Route};
8use crate::response::BodyError;
9use crate::types::List;
10use crate::{Client, Error};
11
12/// Extension trait for typed request builder objects for [`List<T>`] responses.
13pub trait Paginate<'a>: private::Sealed {
14    type Output;
15
16    /// # Examples
17    ///
18    /// ```no_run
19    /// # #[tokio::main]
20    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
21    /// #     let client = modio::Client::builder(std::env::var("MODIO_API_KEY")?).build()?;
22    /// use modio::types::id::Id;
23    /// use modio::types::mods::Mod;
24    /// use modio::util::Paginate;
25    ///
26    /// let mods = client.get_mods(Id::new(51));
27    /// let mut paged = mods.paged();
28    ///
29    /// while let Some(page) = paged.next().await? {
30    ///     for mod_ in page {
31    ///         println!("name: {}", mod_.name);
32    ///     }
33    /// }
34    /// #     Ok(())
35    /// # }
36    /// ```
37    fn paged(&'a self) -> Paginator<'a, Self::Output>;
38}
39
40pub struct Paginator<'a, T> {
41    http: &'a Client,
42    route: Route,
43    filter: Filter,
44    state: State,
45    phantom: PhantomData<T>,
46}
47
48/// The errors that may occur when using [`Paginator::next()`].
49#[derive(Debug)]
50pub enum PaginateError {
51    Request(Error),
52    Body(BodyError),
53}
54
55impl fmt::Display for PaginateError {
56    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57        match self {
58            Self::Request(err) => err.fmt(f),
59            Self::Body(err) => err.fmt(f),
60        }
61    }
62}
63
64impl std::error::Error for PaginateError {}
65
66#[derive(Debug)]
67pub struct Page<T>(List<T>);
68
69impl<T> std::ops::Deref for Page<T> {
70    type Target = List<T>;
71
72    fn deref(&self) -> &Self::Target {
73        &self.0
74    }
75}
76
77impl<T> IntoIterator for Page<T> {
78    type Item = T;
79    type IntoIter = IntoIter<T>;
80
81    fn into_iter(self) -> Self::IntoIter {
82        self.0.data.into_iter()
83    }
84}
85
86enum State {
87    Start,
88    Next { offset: u32, limit: u32 },
89    Completed,
90}
91
92impl<'a, T: DeserializeOwned + Unpin> Paginator<'a, T> {
93    pub(crate) fn new(http: &'a Client, route: Route, filter: Option<Filter>) -> Self {
94        Self {
95            http,
96            route,
97            filter: filter.unwrap_or_default(),
98            state: State::Start,
99            phantom: PhantomData,
100        }
101    }
102
103    pub async fn next(&mut self) -> Result<Option<Page<T>>, PaginateError> {
104        let state = std::mem::replace(&mut self.state, State::Completed);
105
106        let filter = self.filter.clone();
107
108        let filter = match state {
109            State::Start => filter,
110            State::Next { offset, limit } => filter.offset((offset + limit) as usize),
111            State::Completed => return Ok(None),
112        };
113
114        let req = RequestBuilder::from_route(&self.route)
115            .filter(filter)
116            .empty()
117            .map_err(PaginateError::Request)?;
118
119        let list = self
120            .http
121            .request::<List<T>>(req)
122            .await
123            .map_err(PaginateError::Request)?
124            .data()
125            .await
126            .map_err(PaginateError::Body)?;
127
128        if list.data.is_empty() {
129            return Ok(None);
130        }
131
132        self.state = State::Next {
133            offset: list.offset,
134            limit: list.limit,
135        };
136
137        Ok(Some(Page(list)))
138    }
139}
140
141mod private {
142    use crate::request::files;
143    use crate::request::games;
144    use crate::request::mods;
145    use crate::request::user;
146
147    pub trait Sealed {}
148
149    impl Sealed for games::GetGames<'_> {}
150    impl Sealed for games::tags::GetGameTags<'_> {}
151
152    impl Sealed for mods::GetMods<'_> {}
153    impl Sealed for mods::comments::GetModComments<'_> {}
154    impl Sealed for mods::dependencies::GetModDependencies<'_> {}
155    impl Sealed for mods::events::GetModEvents<'_> {}
156    impl Sealed for mods::events::GetModsEvents<'_> {}
157    impl Sealed for mods::stats::GetModsStats<'_> {}
158    impl Sealed for mods::tags::GetModTags<'_> {}
159
160    impl Sealed for files::GetFiles<'_> {}
161    impl Sealed for files::multipart::GetMultipartUploadParts<'_> {}
162    impl Sealed for files::multipart::GetMultipartUploadSessions<'_> {}
163
164    impl Sealed for user::GetMutedUsers<'_> {}
165    impl Sealed for user::GetUserFiles<'_> {}
166    impl Sealed for user::GetUserGames<'_> {}
167    impl Sealed for user::GetUserMods<'_> {}
168    impl Sealed for user::GetUserRatings<'_> {}
169    impl Sealed for user::GetUserSubscriptions<'_> {}
170}