1use std::error::Error;
2use std::future::Future;
3
4use bytes::Bytes;
5use http::{Request, Response};
6use serde::de::DeserializeOwned;
7use serde_json::Value;
8use thiserror::Error;
9
10use crate::error::ApiErrorKind;
11use crate::request::Params;
12
13pub trait Client {
19 type Error: Error + Send + Sync + 'static;
20
21 fn execute(
22 &self,
23 req: Request<Bytes>,
24 ) -> impl Future<Output = Result<Response<Bytes>, Self::Error>> + Send;
25}
26
27pub trait Endpoint {
30 type Response: DeserializeOwned;
31
32 fn method(&self) -> &'static str;
34
35 fn params(&self) -> Params;
37
38 fn parse_response(&self, body: &[u8]) -> Result<Self::Response, ParseError> {
42 let method = self.method();
43 let value: Value = serde_json::from_slice(body).map_err(|_| ParseError::NonJsonBody {
44 body: String::from_utf8_lossy(body).into_owned(),
45 })?;
46 if value.get("result").and_then(Value::as_str) == Some("error") {
47 let message = value
48 .get("message")
49 .and_then(Value::as_str)
50 .unwrap_or("unknown error")
51 .to_string();
52 return Err(ParseError::Api {
53 kind: ApiErrorKind::classify(&message),
54 message,
55 });
56 }
57 serde_json::from_value(value).map_err(|source| ParseError::Decode { source, method })
58 }
59}
60
61#[derive(Debug, Error)]
64pub enum ParseError {
65 #[error("matomo api error: {message}")]
66 Api { message: String, kind: ApiErrorKind },
67 #[error("failed to decode {method} response: {source}")]
68 Decode {
69 source: serde_json::Error,
70 method: &'static str,
71 },
72 #[error("non-json body: {body}")]
73 NonJsonBody { body: String },
74}
75
76pub trait Query<C> {
78 type Result;
79 fn execute(self, client: &C) -> impl Future<Output = Self::Result> + Send;
80}
81
82#[derive(Debug, Error)]
84#[non_exhaustive]
85pub enum QueryError<E>
86where
87 E: Error + Send + Sync + 'static,
88{
89 #[error("transport error: {source}")]
91 Transport { source: E },
92
93 #[error("matomo api error in {method}: {message}")]
95 Api {
96 message: String,
97 method: &'static str,
98 kind: ApiErrorKind,
99 },
100
101 #[error("non-json body from {method}: {body}")]
103 NonJsonBody { method: &'static str, body: String },
104
105 #[error("failed to decode {method} response: {source}")]
107 Decode {
108 source: serde_json::Error,
109 method: &'static str,
110 },
111
112 #[error("failed to build request: {source}")]
114 Build {
115 #[from]
116 source: http::Error,
117 },
118}
119
120impl<E> QueryError<E>
121where
122 E: Error + Send + Sync + 'static,
123{
124 pub fn transport(source: E) -> Self {
125 QueryError::Transport { source }
126 }
127}
128
129const DISPATCH_PATH: &str = "/index.php";
130
131impl<T, C> Query<C> for T
132where
133 T: Endpoint + Send + Sync,
134 C: Client + Send + Sync,
135{
136 type Result = Result<T::Response, QueryError<C::Error>>;
137
138 async fn execute(self, client: &C) -> Self::Result {
139 let method = self.method();
140
141 let mut form: Vec<(&str, &str)> =
142 vec![("module", "API"), ("method", method), ("format", "json")];
143 let params = self.params();
144 for (k, v) in params.fields() {
145 form.push((k.as_str(), v.as_str()));
146 }
147 let body: Bytes = serde_urlencoded::to_string(&form)
148 .map(Bytes::from)
149 .unwrap_or_default();
150
151 let http_req = http::Request::builder()
152 .method(http::Method::POST)
153 .uri(DISPATCH_PATH)
154 .header("Content-Type", "application/x-www-form-urlencoded")
155 .header("Accept", "application/json")
156 .body(body)?;
157
158 let response = client
159 .execute(http_req)
160 .await
161 .map_err(QueryError::transport)?;
162
163 self.parse_response(response.body()).map_err(|e| match e {
164 ParseError::Api { message, kind } => QueryError::Api {
165 message,
166 method,
167 kind,
168 },
169 ParseError::Decode { source, method } => QueryError::Decode { source, method },
170 ParseError::NonJsonBody { body } => QueryError::NonJsonBody { method, body },
171 })
172 }
173}