use std::error::Error;
use std::future::Future;
use bytes::Bytes;
use http::{Request, Response};
use serde::de::DeserializeOwned;
use serde_json::Value;
use thiserror::Error;
use crate::error::ApiErrorKind;
use crate::request::Params;
pub trait Client {
type Error: Error + Send + Sync + 'static;
fn execute(
&self,
req: Request<Bytes>,
) -> impl Future<Output = Result<Response<Bytes>, Self::Error>> + Send;
}
pub trait Endpoint {
type Response: DeserializeOwned;
fn method(&self) -> &'static str;
fn params(&self) -> Params;
fn parse_response(&self, body: &[u8]) -> Result<Self::Response, ParseError> {
let method = self.method();
let value: Value = serde_json::from_slice(body).map_err(|_| ParseError::NonJsonBody {
body: String::from_utf8_lossy(body).into_owned(),
})?;
if value.get("result").and_then(Value::as_str) == Some("error") {
let message = value
.get("message")
.and_then(Value::as_str)
.unwrap_or("unknown error")
.to_string();
return Err(ParseError::Api {
kind: ApiErrorKind::classify(&message),
message,
});
}
serde_json::from_value(value).map_err(|source| ParseError::Decode { source, method })
}
}
#[derive(Debug, Error)]
pub enum ParseError {
#[error("matomo api error: {message}")]
Api { message: String, kind: ApiErrorKind },
#[error("failed to decode {method} response: {source}")]
Decode {
source: serde_json::Error,
method: &'static str,
},
#[error("non-json body: {body}")]
NonJsonBody { body: String },
}
pub trait Query<C> {
type Result;
fn execute(self, client: &C) -> impl Future<Output = Self::Result> + Send;
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum QueryError<E>
where
E: Error + Send + Sync + 'static,
{
#[error("transport error: {source}")]
Transport { source: E },
#[error("matomo api error in {method}: {message}")]
Api {
message: String,
method: &'static str,
kind: ApiErrorKind,
},
#[error("non-json body from {method}: {body}")]
NonJsonBody { method: &'static str, body: String },
#[error("failed to decode {method} response: {source}")]
Decode {
source: serde_json::Error,
method: &'static str,
},
#[error("failed to build request: {source}")]
Build {
#[from]
source: http::Error,
},
}
impl<E> QueryError<E>
where
E: Error + Send + Sync + 'static,
{
pub fn transport(source: E) -> Self {
QueryError::Transport { source }
}
}
const DISPATCH_PATH: &str = "/index.php";
impl<T, C> Query<C> for T
where
T: Endpoint + Send + Sync,
C: Client + Send + Sync,
{
type Result = Result<T::Response, QueryError<C::Error>>;
async fn execute(self, client: &C) -> Self::Result {
let method = self.method();
let mut form: Vec<(&str, &str)> =
vec![("module", "API"), ("method", method), ("format", "json")];
let params = self.params();
for (k, v) in params.fields() {
form.push((k.as_str(), v.as_str()));
}
let body: Bytes = serde_urlencoded::to_string(&form)
.map(Bytes::from)
.unwrap_or_default();
let http_req = http::Request::builder()
.method(http::Method::POST)
.uri(DISPATCH_PATH)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Accept", "application/json")
.body(body)?;
let response = client
.execute(http_req)
.await
.map_err(QueryError::transport)?;
self.parse_response(response.body()).map_err(|e| match e {
ParseError::Api { message, kind } => QueryError::Api {
message,
method,
kind,
},
ParseError::Decode { source, method } => QueryError::Decode { source, method },
ParseError::NonJsonBody { body } => QueryError::NonJsonBody { method, body },
})
}
}