mastodon_api/
paging.rs

1use crate::MastodonClient;
2use crate::error::Result;
3use serde::de::DeserializeOwned;
4
5/// A request that supports automatic pagination via the `Link` header.
6///
7/// This is used to fetch multiple pages of results (e.g., historical timelines)
8/// by following the "next" link provided by the server.
9pub struct PagedRequest<'a, T> {
10    client: &'a MastodonClient,
11    next_url: Option<String>,
12    _marker: std::marker::PhantomData<T>,
13}
14
15impl<'a, T: DeserializeOwned> PagedRequest<'a, T> {
16    /// Creates a new `PagedRequest` starting at the given URL.
17    pub fn new(client: &'a MastodonClient, initial_url: String) -> Self {
18        Self {
19            client,
20            next_url: Some(initial_url),
21            _marker: std::marker::PhantomData,
22        }
23    }
24
25    /// Fetches the next page of results.
26    ///
27    /// Returns `Ok(Some(Vec<T>))` if a page was successfully fetched,
28    /// or `Ok(None)` if there are no more pages.
29    pub async fn next_page(&mut self) -> Result<Option<Vec<T>>> {
30        let url = match &self.next_url {
31            Some(u) => u,
32            None => return Ok(None),
33        };
34
35        let response = self.client.http_client().get(url);
36        let builder = response;
37
38        let mut req_builder = builder;
39        if let Some(token) = self.client.access_token() {
40            req_builder = req_builder.bearer_auth(token);
41        }
42
43        let resp = req_builder.send().await?;
44
45        if !resp.status().is_success() {
46            return Err(crate::error::MastodonError::ApiError {
47                status: resp.status(),
48                message: resp.text().await.unwrap_or_default(),
49            });
50        }
51
52        // Parse Link header
53        self.next_url = parse_link_header(resp.headers().get("Link"));
54
55        Ok(Some(resp.json().await?))
56    }
57}
58
59/// Helper to parse the `Link` header into a URL string for the next page.
60fn parse_link_header(header: Option<&reqwest::header::HeaderValue>) -> Option<String> {
61    let header_str = header?.to_str().ok()?;
62    // Example: <url>; rel="next", <url>; rel="prev"
63    for part in header_str.split(',') {
64        if part.contains("rel=\"next\"") {
65            let start = part.find('<')? + 1;
66            let end = part.find('>')?;
67            return Some(part[start..end].to_string());
68        }
69    }
70    None
71}