mastodon_async/
page.rs

1use super::{Mastodon, Result};
2use crate::{entities::itemsiter::ItemsIter, helpers::read_response::read_response, Error};
3use futures::Stream;
4use log::{as_debug, as_serde, debug, error, trace};
5use reqwest::{header::LINK, Response, Url};
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9macro_rules! pages {
10    ($($direction:ident: $fun:ident),*) => {
11
12        $(
13            doc_comment!(concat!(
14                    "Method to retrieve the ", stringify!($direction), " page of results",
15                    "Returns Ok(None) if there is no data in the ", stringify!($direction), " page.\n",
16                "Returns Ok(Some(Vec<T>)) if there are results.\n",
17                "Returns Err(Error) if there is an error.\n",
18                "If there are results, the next and previous page urls are stored.\n",
19                "If there are no results, the next and previous page urls are not stored.\n",
20                "This allows for the next page to be retrieved in the future even when\n",
21                "there are no results.",
22                ),
23            pub async fn $fun(&mut self) -> Result<Option<Vec<T>>> {
24                let Some(ref url) = self.$direction else {
25                    return Ok(None);
26                };
27
28                debug!(
29                    url = url.as_str(), method = "get",
30                    call_id = as_debug!(self.call_id),
31                    direction = stringify!($direction);
32                    "making API request"
33                );
34                let url: String = url.to_string();
35                let response = self.mastodon.authenticated(self.mastodon.client.get(&url)).send().await?;
36                match response.error_for_status() {
37                    Ok(response) => {
38                        let (prev, next) = get_links(&response, self.call_id)?;
39                        let response: Vec<T> = read_response(response).await?;
40                        if response.is_empty() && prev.is_none() && next.is_none() {
41                            debug!(
42                                url = url, method = "get", call_id = as_debug!(self.call_id),
43                                direction = stringify!($direction);
44                                "received an empty page with no links"
45                            );
46                            return Ok(None);
47                        }
48                        debug!(
49                            url = url, method = "get",call_id = as_debug!(self.call_id),
50                            direction = stringify!($direction),
51                            prev = as_debug!(prev),
52                            next = as_debug!(next),
53                            response = as_serde!(response);
54                            "received next pages from API"
55                        );
56                        self.next = next;
57                        self.prev = prev;
58                        Ok(Some(response))
59                    }
60                    Err(err) => {
61                        error!(
62                            err = as_debug!(err), url = url,
63                            method = stringify!($method),
64                            call_id = as_debug!(self.call_id);
65                            "error making API request"
66                        );
67                        Err(err.into())
68                    }
69                }
70
71            });
72         )*
73    }
74}
75
76/// Owned version of the `Page` struct in this module. Allows this to be more
77/// easily stored for later use
78///
79/// // Example
80///
81/// ```no_run
82/// use mastodon_async::{
83///     prelude::*,
84///     page::Page,
85///     entities::status::Status
86/// };
87/// use std::cell::RefCell;
88///
89/// tokio_test::block_on(async {
90///     let data = Data::default();
91///     struct HomeTimeline {
92///         client: Mastodon,
93///         page: RefCell<Option<Page<Status>>>,
94///     }
95///     let client = Mastodon::from(data);
96///     let home = client.get_home_timeline().await.unwrap();
97///     let tl = HomeTimeline {
98///         client,
99///         page: RefCell::new(Some(home)),
100///     };
101/// });
102/// ```
103
104/// Represents a single page of API results
105#[derive(Debug, Clone)]
106pub struct Page<T: for<'de> Deserialize<'de> + Serialize> {
107    mastodon: Mastodon,
108    /// next url
109    pub next: Option<Url>,
110    /// prev url
111    pub prev: Option<Url>,
112    /// Initial set of items
113    pub initial_items: Vec<T>,
114    pub(crate) call_id: Uuid,
115}
116
117impl<'a, T: for<'de> Deserialize<'de> + Serialize> Page<T> {
118    pages! {
119        next: next_page,
120        prev: prev_page
121    }
122
123    /// Create a new Page.
124    pub(crate) async fn new(mastodon: Mastodon, response: Response, call_id: Uuid) -> Result<Self> {
125        let status = response.status();
126        if status.is_success() {
127            let (prev, next) = get_links(&response, call_id)?;
128            let initial_items = read_response(response).await?;
129            debug!(
130                initial_items = as_serde!(initial_items), prev = as_debug!(prev),
131                next = as_debug!(next), call_id = as_debug!(call_id);
132                "received first page from API call"
133            );
134            Ok(Page {
135                initial_items,
136                next,
137                prev,
138                mastodon,
139                call_id,
140            })
141        } else {
142            let response = response.json().await?;
143            Err(Error::Api { status, response })
144        }
145    }
146}
147
148impl<T: Clone + for<'de> Deserialize<'de> + Serialize> Page<T> {
149    /// Returns an iterator that provides a stream of `T`s
150    ///
151    /// This abstracts away the process of iterating over each item in a page,
152    /// then making an http call, then iterating over each item in the new
153    /// page, etc. The iterator provides a stream of `T`s, calling
154    /// `self.next_page()`
155    /// when necessary to get
156    /// more of them, until
157    /// there are no more items.
158    ///
159    /// // Example
160    ///
161    /// ```no_run
162    /// use mastodon_async::prelude::*;
163    /// use futures_util::StreamExt;
164    ///
165    /// let data = Data::default();
166    /// let mastodon = Mastodon::from(data);
167    /// let req = StatusesRequest::new();
168    ///
169    /// tokio_test::block_on(async {
170    ///     let resp = mastodon.statuses(&AccountId::new("some-id"), req).await.unwrap();
171    ///     resp.items_iter().for_each(|status| async move {
172    ///         // do something with status
173    ///     }).await;
174    /// });
175    /// ```
176    pub fn items_iter(self) -> impl Stream<Item = T> {
177        ItemsIter::new(self).stream()
178    }
179}
180
181fn get_links(response: &Response, call_id: Uuid) -> Result<(Option<Url>, Option<Url>)> {
182    let mut prev = None;
183    let mut next = None;
184
185    if let Some(link_header) = response.headers().get(LINK) {
186        let link_header = link_header.to_str()?;
187        let raw_link_header = link_header.to_string();
188        trace!(link_header = link_header, call_id = as_debug!(call_id); "parsing link header");
189        let link_header = parse_link_header::parse(link_header)?;
190        for (rel, link) in link_header.iter() {
191            match rel.as_ref().map(|it| it.as_str()) {
192                Some("next") => next = Some(link.uri.clone()),
193                Some("prev") => prev = Some(link.uri.clone()),
194                None => debug!(link = as_debug!(link); "link header with no rel specified"),
195                Some(other) => {
196                    return Err(Error::UnrecognizedRel {
197                        rel: other.to_string(),
198                        link: raw_link_header,
199                    })
200                }
201            }
202        }
203    }
204
205    Ok((prev, next))
206}