readwhitepaper 0.1.1

Rust client for ReadWhitepaper — blockchain whitepaper database with 30 whitepapers, 163 glossary terms, and 15-language translations
Documentation
//! ReadWhitepaper API client — async, typed, powered by reqwest.

use reqwest::Client as HttpClient;

use crate::types::*;

/// Error type for ReadWhitepaper API operations.
#[derive(Debug)]
pub enum Error {
    /// HTTP request failed.
    Http(reqwest::Error),
    /// API returned an error status code.
    Api { status: u16, message: String },
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Error::Http(e) => write!(f, "readwhitepaper: HTTP error: {e}"),
            Error::Api { status, message } => {
                write!(f, "readwhitepaper: API error {status}: {message}")
            }
        }
    }
}

impl std::error::Error for Error {}

impl From<reqwest::Error> for Error {
    fn from(err: reqwest::Error) -> Self {
        Error::Http(err)
    }
}

/// Client for the ReadWhitepaper blockchain whitepaper API.
///
/// Provides async access to 30 cryptocurrency whitepapers,
/// 163 glossary terms, and full-text search across 15 languages.
pub struct ReadWhitepaperClient {
    base_url: String,
    language: String,
    http: HttpClient,
}

impl Default for ReadWhitepaperClient {
    fn default() -> Self {
        Self::new()
    }
}

impl ReadWhitepaperClient {
    /// Create a new client with default settings.
    pub fn new() -> Self {
        Self {
            base_url: "https://readwhitepaper.com/api".to_string(),
            language: "en".to_string(),
            http: HttpClient::builder()
                .user_agent("readwhitepaper-rs/0.1.0")
                .timeout(std::time::Duration::from_secs(30))
                .build()
                .expect("failed to build HTTP client"),
        }
    }

    /// Create a client with a custom base URL.
    pub fn with_base_url(mut self, base_url: &str) -> Self {
        self.base_url = base_url.trim_end_matches('/').to_string();
        self
    }

    /// Set the default language.
    pub fn with_language(mut self, language: &str) -> Self {
        self.language = language.to_string();
        self
    }

    async fn get<T: serde::de::DeserializeOwned>(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> Result<T, Error> {
        let url = format!("{}{}", self.base_url, path);
        let resp = self.http.get(&url).query(params).send().await?;

        if !resp.status().is_success() {
            let status = resp.status().as_u16();
            let message = resp.text().await.unwrap_or_default();
            return Err(Error::Api {
                status,
                message: message.chars().take(200).collect(),
            });
        }

        Ok(resp.json().await?)
    }

    fn lang<'a>(&'a self, language: Option<&'a str>) -> &'a str {
        language.unwrap_or(&self.language)
    }

    // ── Whitepapers ──────────────────────────────────────────

    /// List all whitepapers with optional pagination.
    pub async fn list_whitepapers(
        &self,
        limit: Option<i32>,
        page: Option<i32>,
        language: Option<&str>,
    ) -> Result<PaginatedResponse<Whitepaper>, Error> {
        let lang = self.lang(language).to_string();
        let limit_s = limit.map(|l| l.to_string());
        let page_s = page.map(|p| p.to_string());
        let mut params: Vec<(&str, &str)> = vec![("language", &lang)];
        if let Some(ref l) = limit_s {
            params.push(("limit", l));
        }
        if let Some(ref p) = page_s {
            params.push(("page", p));
        }
        self.get("/whitepapers/", &params).await
    }

    /// Get full whitepaper detail with all sections.
    pub async fn get_whitepaper(
        &self,
        slug: &str,
        language: Option<&str>,
    ) -> Result<WhitepaperDetail, Error> {
        let lang = self.lang(language).to_string();
        let path = format!("/whitepapers/{}/", slug);
        self.get(&path, &[("language", &lang)]).await
    }

    /// Get whitepaper sections.
    pub async fn get_sections(
        &self,
        slug: &str,
        language: Option<&str>,
    ) -> Result<Vec<Section>, Error> {
        let lang = self.lang(language).to_string();
        let path = format!("/whitepapers/{}/sections/", slug);
        self.get(&path, &[("language", &lang)]).await
    }

    /// Get table of contents for a whitepaper.
    pub async fn get_toc(
        &self,
        slug: &str,
        language: Option<&str>,
    ) -> Result<Vec<TocEntry>, Error> {
        let lang = self.lang(language).to_string();
        let path = format!("/whitepapers/{}/toc/", slug);
        let resp: TocResponse = self.get(&path, &[("language", &lang)]).await?;
        Ok(resp.toc)
    }

    /// Get translation coverage for a whitepaper.
    pub async fn get_coverage(&self, slug: &str) -> Result<Coverage, Error> {
        let path = format!("/whitepapers/{}/coverage/", slug);
        self.get(&path, &[]).await
    }

    // ── Glossary ─────────────────────────────────────────────

    /// List blockchain glossary terms with pagination.
    pub async fn list_glossary(
        &self,
        limit: Option<i32>,
        page: Option<i32>,
        language: Option<&str>,
    ) -> Result<PaginatedResponse<GlossaryTerm>, Error> {
        let lang = self.lang(language).to_string();
        let limit_s = limit.map(|l| l.to_string());
        let page_s = page.map(|p| p.to_string());
        let mut params: Vec<(&str, &str)> = vec![("language", &lang)];
        if let Some(ref l) = limit_s {
            params.push(("limit", l));
        }
        if let Some(ref p) = page_s {
            params.push(("page", p));
        }
        self.get("/glossary/", &params).await
    }

    /// Get a single glossary term by slug.
    pub async fn get_glossary_term(
        &self,
        slug: &str,
        language: Option<&str>,
    ) -> Result<GlossaryTerm, Error> {
        let lang = self.lang(language).to_string();
        let path = format!("/glossary/{}/", slug);
        self.get(&path, &[("language", &lang)]).await
    }

    // ── Search ───────────────────────────────────────────────

    /// Full-text search across all whitepapers.
    pub async fn search(
        &self,
        query: &str,
        slug: Option<&str>,
        language: Option<&str>,
    ) -> Result<Vec<SearchResult>, Error> {
        let lang = self.lang(language).to_string();
        let mut params: Vec<(&str, &str)> = vec![("q", query), ("language", &lang)];
        if let Some(s) = slug {
            params.push(("slug", s));
        }
        let resp: SearchResponse = self.get("/search/", &params).await?;
        Ok(resp.results)
    }

    // ── Stats / Languages / Graph ────────────────────────────

    /// Get platform statistics.
    pub async fn get_stats(&self) -> Result<Stats, Error> {
        self.get("/stats/", &[]).await
    }

    /// Get all supported languages.
    pub async fn get_languages(&self) -> Result<Vec<Language>, Error> {
        let resp: LanguagesResponse = self.get("/languages/", &[]).await?;
        Ok(resp.results)
    }

    /// Get the whitepaper relationship graph.
    pub async fn get_graph(&self) -> Result<GraphData, Error> {
        self.get("/graph/", &[]).await
    }
}