matomo-rs 0.1.0

Async client for the Matomo Reporting API, focused on data export and migration
Documentation
use thiserror::Error;

pub type Result<T> = std::result::Result<T, Error>;

#[non_exhaustive]
#[derive(Debug, Error)]
pub enum Error {
    /// Transport-level failure from the bundled `reqwest` transport. Only
    /// present with the `reqwest` feature; an accepted semver coupling.
    #[cfg(feature = "reqwest")]
    #[error("http transport error: {0}")]
    Http(#[from] reqwest::Error),

    /// Matomo returned `{"result":"error", ...}` with HTTP 200.
    #[error("matomo api error in {method}: {message}")]
    Api {
        message: String,
        method: &'static str,
        kind: ApiErrorKind,
    },

    /// The body was valid JSON but did not match the expected typed shape.
    #[error("failed to decode {method} response: {source}")]
    Decode {
        #[source]
        source: serde_json::Error,
        method: &'static str,
    },

    /// The body was not JSON at all (e.g. an HTML error page).
    #[error("non-json body from {method}: {body}")]
    NonJsonBody { method: &'static str, body: String },

    /// Misconfiguration detected while building the client.
    #[error("configuration error: {0}")]
    Config(String),

    /// A preflight check failed.
    #[error("preflight failed: {0}")]
    Preflight(String),
}

/// Best-effort classification of an API error, sniffed from the message text.
/// This is a heuristic; Matomo does not return machine-readable error codes.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiErrorKind {
    Auth,
    UnknownMethod,
    InvalidParam,
    RateLimited,
    NoData,
    Other,
}

impl ApiErrorKind {
    pub(crate) fn classify(message: &str) -> Self {
        let m = message.to_ascii_lowercase();
        if m.contains("token_auth")
            || m.contains("authentication")
            || m.contains("not allowed")
            || m.contains("permission")
        {
            ApiErrorKind::Auth
        } else if m.contains("method") && (m.contains("not exist") || m.contains("not found")) {
            ApiErrorKind::UnknownMethod
        } else if m.contains("requires") || m.contains("parameter") || m.contains("invalid") {
            ApiErrorKind::InvalidParam
        } else if m.contains("rate") && m.contains("limit") {
            ApiErrorKind::RateLimited
        } else if m.contains("no data") {
            ApiErrorKind::NoData
        } else {
            ApiErrorKind::Other
        }
    }
}