flarer 0.1.0

Rust client and CLI for Cloudflare's Browser Rendering REST API (content, screenshot, PDF, snapshot, markdown, scrape, JSON extraction, links, crawl).
Documentation
//! Cloudflare account credentials.

use secrecy::{ExposeSecret, SecretString};
use serde::{Deserialize, Serialize};

/// Cloudflare account credentials used by [`Flarer`](crate::Flarer).
///
/// The API token is wrapped in [`SecretString`] so it is not leaked through
/// `Debug` output or accidental logging.
#[derive(Clone, Serialize, Deserialize)]
pub struct Account {
    /// Cloudflare account ID (`CF_ACCOUNT_ID`).
    pub id: String,
    /// API token (`CF_API_TOKEN`). Kept opaque via [`SecretString`].
    #[serde(
        serialize_with = "serialize_token",
        deserialize_with = "deserialize_token"
    )]
    token: SecretString,
}

impl Account {
    /// Build an [`Account`] from an account id and API token.
    pub fn new(id: impl Into<String>, token: impl Into<String>) -> Self {
        Self {
            id: id.into(),
            token: SecretString::new(token.into().into()),
        }
    }

    /// Build an [`Account`] from the `CF_ACCOUNT_ID` and `CF_API_TOKEN`
    /// environment variables.
    ///
    /// Returns [`crate::FlarerError::Config`] if either variable is missing
    /// or empty.
    pub fn from_env() -> crate::Result<Self> {
        let id = std::env::var("CF_ACCOUNT_ID")
            .map_err(|_| crate::FlarerError::Config("CF_ACCOUNT_ID not set".into()))?;
        let token = std::env::var("CF_API_TOKEN")
            .map_err(|_| crate::FlarerError::Config("CF_API_TOKEN not set".into()))?;
        if id.is_empty() || token.is_empty() {
            return Err(crate::FlarerError::Config(
                "CF_ACCOUNT_ID / CF_API_TOKEN must not be empty".into(),
            ));
        }
        Ok(Self::new(id, token))
    }

    /// Borrow the raw token string. Use sparingly — prefer passing the
    /// [`Account`] around as a whole.
    pub fn token(&self) -> &str {
        self.token.expose_secret()
    }
}

impl std::fmt::Debug for Account {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Account")
            .field("id", &self.id)
            .field("token", &"<redacted>")
            .finish()
    }
}

fn serialize_token<S: serde::Serializer>(
    s: &SecretString,
    ser: S,
) -> std::result::Result<S::Ok, S::Error> {
    ser.serialize_str(s.expose_secret())
}

fn deserialize_token<'de, D: serde::Deserializer<'de>>(
    de: D,
) -> std::result::Result<SecretString, D::Error> {
    let raw = String::deserialize(de)?;
    Ok(SecretString::new(raw.into()))
}