bitculator 1.1.0

Official Rust SDK for the Bitculator Data API
Documentation
use std::sync::Arc;

use reqwest::Method;
use serde::de::DeserializeOwned;
use serde::Deserialize;

use crate::error::Error;
use crate::http::HttpClient;
use crate::params::Params;

/// Laravel paginator metadata on every paginated list response.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct PageMeta {
    #[serde(default)]
    pub current_page: u32,
    pub from: Option<u32>,
    #[serde(default)]
    pub last_page: u32,
    #[serde(default)]
    pub per_page: u32,
    pub to: Option<u32>,
    #[serde(default)]
    pub total: u64,
}

/// Navigation links on every paginated list response.
#[derive(Debug, Clone, Default, Deserialize)]
pub struct PageLinks {
    pub first: Option<String>,
    pub last: Option<String>,
    pub prev: Option<String>,
    pub next: Option<String>,
}

#[derive(Deserialize)]
struct Envelope<T> {
    // Option makes both a missing `data` field and a literal `data: null`
    // deserialize to None (serde treats Option fields as implicitly optional;
    // a #[serde(default)] here would wrongly infer a `T: Default` bound).
    data: Option<Vec<T>>,
    meta: Option<PageMeta>,
    links: Option<PageLinks>,
}

/// One page of a paginated list, plus lazy forward navigation. `data` is the
/// current page's rows; `auto_paging()` walks every following page.
pub struct Page<T> {
    pub data: Vec<T>,
    pub meta: PageMeta,
    pub links: PageLinks,
    http: Arc<HttpClient>,
    path: String,
    params: Params,
}

impl<T: DeserializeOwned> Page<T> {
    pub(crate) fn fetch(http: Arc<HttpClient>, path: &str, params: Params) -> Result<Self, Error> {
        let value = http.send(Method::GET, path, Some(&params), None)?;
        let env: Envelope<T> =
            serde_json::from_value(value).map_err(|e| Error::Decode(e.to_string()))?;
        Ok(Page {
            data: env.data.unwrap_or_default(),
            meta: env.meta.unwrap_or_default(),
            links: env.links.unwrap_or_default(),
            http,
            path: path.to_string(),
            params,
        })
    }

    /// True if at least one more page exists.
    pub fn has_more(&self) -> bool {
        if self.links.next.as_deref().is_some_and(|s| !s.is_empty()) {
            return true;
        }
        self.meta.current_page != 0 && self.meta.last_page != 0 && self.meta.current_page < self.meta.last_page
    }

    /// Fetch the next page, or `Ok(None)` if this is the last one.
    pub fn next_page(&self) -> Result<Option<Page<T>>, Error> {
        if !self.has_more() {
            return Ok(None);
        }
        let params = self.params.with_page(self.meta.current_page + 1);
        Page::fetch(self.http.clone(), &self.path, params).map(Some)
    }

    /// Consume this page into an iterator over every row across this and all
    /// following pages, fetching lazily. Each item is a `Result`.
    pub fn auto_paging(self) -> AutoPager<T> {
        let next_params = if self.has_more() {
            Some(self.params.with_page(self.meta.current_page + 1))
        } else {
            None
        };
        AutoPager {
            http: self.http,
            path: self.path,
            next_params,
            current: self.data.into_iter(),
        }
    }
}

/// Iterator returned by [`Page::auto_paging`].
pub struct AutoPager<T> {
    http: Arc<HttpClient>,
    path: String,
    next_params: Option<Params>,
    current: std::vec::IntoIter<T>,
}

impl<T: DeserializeOwned> Iterator for AutoPager<T> {
    type Item = Result<T, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        loop {
            if let Some(item) = self.current.next() {
                return Some(Ok(item));
            }
            let params = self.next_params.take()?;
            match Page::<T>::fetch(self.http.clone(), &self.path, params) {
                Ok(page) => {
                    self.next_params = if page.has_more() {
                        Some(page.params.with_page(page.meta.current_page + 1))
                    } else {
                        None
                    };
                    self.current = page.data.into_iter();
                }
                Err(e) => return Some(Err(e)),
            }
        }
    }
}