use std::sync::Mutex;
use serde_json::Value;
use crate::auth::Auth;
use crate::endpoints::{ApiGetMatomoVersion, ApiGetReportMetadata};
use crate::error::{Error, Result};
use crate::request::Params;
use crate::reqwest::MatomoClient;
const REQUIRED_METADATA_METHODS: &[(&str, &str)] = &[("VisitsSummary", "get")];
#[derive(Default)]
pub(crate) struct PreflightState {
done: Mutex<Option<std::result::Result<(), String>>>,
}
fn minimum_version(auth: &Auth) -> (u32, u32) {
match auth {
Auth::Bearer(_) => (5, 4),
Auth::Token(_) => (5, 0),
}
}
fn parse_major_minor(version: &str) -> Option<(u32, u32)> {
let mut parts = version.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next().unwrap_or("0").parse().ok()?;
Some((major, minor))
}
pub(crate) async fn run(
client: &MatomoClient,
method: &'static str,
id_site: Option<&str>,
) -> Result<()> {
if let Some(prev) = client.inner().preflight.done.lock().unwrap().clone() {
return prev.map_err(Error::Preflight);
}
let outcome = perform(client, method, id_site).await;
let stored = match &outcome {
Ok(()) => Ok(()),
Err(e) => Err(e.to_string()),
};
*client.inner().preflight.done.lock().unwrap() = Some(stored);
outcome
}
async fn perform(client: &MatomoClient, method: &'static str, id_site: Option<&str>) -> Result<()> {
let value = client.query_unchecked(ApiGetMatomoVersion).await?;
let version = value
.get("value")
.and_then(Value::as_str)
.ok_or_else(|| Error::Preflight("getMatomoVersion returned no value".to_string()))?;
let got = parse_major_minor(version)
.ok_or_else(|| Error::Preflight(format!("unparseable version: {version}")))?;
let want = minimum_version(&client.inner().auth);
if got < want {
return Err(Error::Preflight(format!(
"Matomo {}.{} is below the required {}.{} (method {method})",
got.0, got.1, want.0, want.1
)));
}
let Some(id_site) = id_site else {
return Ok(());
};
let value = client
.query_unchecked(ApiGetReportMetadata {
params: Params::new().set("idSite", id_site),
})
.await?;
let reports = value
.as_array()
.ok_or_else(|| Error::Preflight("getReportMetadata was not an array".to_string()))?;
for (module, action) in REQUIRED_METADATA_METHODS {
let present = reports.iter().any(|r| {
r.get("module").and_then(Value::as_str) == Some(*module)
&& r.get("action").and_then(Value::as_str) == Some(*action)
});
if !present {
return Err(Error::Preflight(format!(
"instance does not expose required report {module}.{action}"
)));
}
}
Ok(())
}