matomo-rs 0.1.0

Async client for the Matomo Reporting API, focused on data export and migration
Documentation
use std::num::NonZeroU32;

/// One or more site ids, or all sites.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IdSite {
    Single(u32),
    Multiple(Vec<u32>),
    All,
}

impl IdSite {
    pub(crate) fn to_param(&self) -> String {
        match self {
            IdSite::Single(id) => id.to_string(),
            IdSite::Multiple(ids) => ids.iter().map(u32::to_string).collect::<Vec<_>>().join(","),
            IdSite::All => "all".to_string(),
        }
    }
}

impl From<u32> for IdSite {
    fn from(id: u32) -> Self {
        IdSite::Single(id)
    }
}

/// A Matomo date in its query grammar.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Date {
    Today,
    Yesterday,
    /// `year, month, day` → `YYYY-MM-DD`.
    Ymd(u16, u8, u8),
    /// `lastN` rolling window.
    LastN(u32),
    /// `previousN` rolling window.
    PreviousN(u32),
}

impl Date {
    pub(crate) fn to_param(self) -> String {
        match self {
            Date::Today => "today".to_string(),
            Date::Yesterday => "yesterday".to_string(),
            Date::Ymd(y, m, d) => format!("{y:04}-{m:02}-{d:02}"),
            Date::LastN(n) => format!("last{n}"),
            Date::PreviousN(n) => format!("previous{n}"),
        }
    }
}

/// The end of an absolute-start range. Matomo accepts an absolute date or the
/// `today`/`yesterday` keywords here, but not `lastN`-style keywords.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RangeEnd {
    Ymd(u16, u8, u8),
    Today,
    Yesterday,
}

impl RangeEnd {
    fn to_param(self) -> String {
        match self {
            RangeEnd::Ymd(y, m, d) => format!("{y:04}-{m:02}-{d:02}"),
            RangeEnd::Today => "today".to_string(),
            RangeEnd::Yesterday => "yesterday".to_string(),
        }
    }
}

/// A date range used with `Period::Range`. Matomo only accepts a single rolling
/// keyword (`lastN`/`previousN`) or an absolute start paired with an absolute or
/// `today`/`yesterday` end — a keyword start like `last7,today` is rejected, so
/// it is unrepresentable here.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DateRange {
    LastN(u32),
    PreviousN(u32),
    Between { from: (u16, u8, u8), to: RangeEnd },
}

impl DateRange {
    /// Convenience constructor for an absolute `YYYY-MM-DD,YYYY-MM-DD` range.
    pub fn ymd(from: (u16, u8, u8), to: (u16, u8, u8)) -> Self {
        DateRange::Between {
            from,
            to: RangeEnd::Ymd(to.0, to.1, to.2),
        }
    }

    pub(crate) fn to_param(self) -> String {
        match self {
            DateRange::LastN(n) => format!("last{n}"),
            DateRange::PreviousN(n) => format!("previous{n}"),
            DateRange::Between {
                from: (y, m, d),
                to,
            } => format!("{y:04}-{m:02}-{d:02},{}", to.to_param()),
        }
    }
}

/// A period paired with the date(s) it applies to. Illegal (period, date)
/// combinations are unrepresentable: `Range` carries its own `DateRange`,
/// the others carry a single `Date`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Period {
    Day(Date),
    Week(Date),
    Month(Date),
    Year(Date),
    Range(DateRange),
}

impl Period {
    /// Returns the `(period, date)` form-field pair.
    pub(crate) fn to_params(self) -> (&'static str, String) {
        match self {
            Period::Day(d) => ("day", d.to_param()),
            Period::Week(d) => ("week", d.to_param()),
            Period::Month(d) => ("month", d.to_param()),
            Period::Year(d) => ("year", d.to_param()),
            Period::Range(r) => ("range", r.to_param()),
        }
    }
}

/// A raw segment expression. Passed verbatim in the POST body; reqwest
/// url-encodes it for us, so do not pre-encode.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Segment(pub String);

impl Segment {
    pub fn new(expr: impl Into<String>) -> Self {
        Segment(expr.into())
    }

    pub(crate) fn as_str(&self) -> &str {
        &self.0
    }
}

impl From<&str> for Segment {
    fn from(s: &str) -> Self {
        Segment(s.to_string())
    }
}

impl From<String> for Segment {
    fn from(s: String) -> Self {
        Segment(s)
    }
}

/// `filter_limit`. `All` maps to `-1`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Limit {
    All,
    Count(NonZeroU32),
}

impl Limit {
    pub fn count(n: u32) -> Option<Self> {
        NonZeroU32::new(n).map(Limit::Count)
    }

    pub(crate) fn to_param(self) -> String {
        match self {
            Limit::All => "-1".to_string(),
            Limit::Count(n) => n.to_string(),
        }
    }

    #[cfg_attr(not(feature = "reqwest"), allow(dead_code))]
    pub(crate) fn is_all(self) -> bool {
        matches!(self, Limit::All)
    }
}