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> {
47 let method = self.method();
48 let value: Value = serde_json::from_slice(body).map_err(|_| ParseError::NonJsonBody {
49 body: body_snippet(body),
50 })?;
51 if value.get("result").and_then(Value::as_str) == Some("error") {
52 let message = value
53 .get("message")
54 .and_then(Value::as_str)
55 .unwrap_or("unknown error")
56 .to_string();
57 return Err(ParseError::Api {
58 kind: ApiErrorKind::classify(&message),
59 message,
60 });
61 }
62 serde_json::from_value(value).map_err(|source| ParseError::Decode { source, method })
63 }
64}
65
66#[derive(Debug, Error)]
69pub enum ParseError {
70 #[error("matomo api error: {message}")]
71 Api { message: String, kind: ApiErrorKind },
72 #[error("failed to decode {method} response: {source}")]
73 Decode {
74 source: serde_json::Error,
75 method: &'static str,
76 },
77 #[error("non-json body: {body}")]
78 NonJsonBody { body: String },
79}
80
81pub trait Query<C> {
83 type Result;
84 fn execute(self, client: &C) -> impl Future<Output = Self::Result> + Send;
85}
86
87#[derive(Debug, Error)]
89#[non_exhaustive]
90pub enum QueryError<E>
91where
92 E: Error + Send + Sync + 'static,
93{
94 #[error("transport error: {source}")]
96 Transport { source: E },
97
98 #[error("matomo api error in {method}: {message}")]
100 Api {
101 message: String,
102 method: &'static str,
103 kind: ApiErrorKind,
104 },
105
106 #[error("non-json body from {method}: {body}")]
109 NonJsonBody { method: &'static str, body: String },
110
111 #[error("failed to decode {method} response: {source}")]
113 Decode {
114 source: serde_json::Error,
115 method: &'static str,
116 },
117
118 #[error("failed to build request: {source}")]
120 Build {
121 #[from]
122 source: http::Error,
123 },
124
125 #[error("failed to encode form body: {source}")]
127 Encode {
128 #[from]
129 source: serde_urlencoded::ser::Error,
130 },
131}
132
133impl<E> QueryError<E>
134where
135 E: Error + Send + Sync + 'static,
136{
137 pub fn transport(source: E) -> Self {
138 QueryError::Transport { source }
139 }
140}
141
142const DISPATCH_PATH: &str = "/index.php";
143
144const BODY_SNIPPET_MAX: usize = 2048;
145
146pub(crate) fn body_snippet(body: &[u8]) -> String {
149 let total = body.len();
150 let cut = total.min(BODY_SNIPPET_MAX);
151 let mut out: String = String::from_utf8_lossy(&body[..cut])
152 .chars()
153 .map(|c| {
154 if c.is_ascii_control() && c != '\n' {
155 ' '
156 } else {
157 c
158 }
159 })
160 .collect();
161 if total > BODY_SNIPPET_MAX {
162 use std::fmt::Write;
163 let _ = write!(out, " ...(truncated, {total} bytes total)");
165 }
166 out
167}
168
169pub(crate) fn build_dispatch_request<E>(
173 method: &str,
174 params: &Params,
175) -> Result<Request<Bytes>, QueryError<E>>
176where
177 E: Error + Send + Sync + 'static,
178{
179 let mut form: Vec<(&str, &str)> =
180 vec![("module", "API"), ("method", method), ("format", "json")];
181 for (k, v) in params.fields() {
182 form.push((k.as_str(), v.as_str()));
183 }
184 let body: Bytes = serde_urlencoded::to_string(&form).map(Bytes::from)?;
185
186 let req = http::Request::builder()
187 .method(http::Method::POST)
188 .uri(DISPATCH_PATH)
189 .header("Content-Type", "application/x-www-form-urlencoded")
190 .header("Accept", "application/json")
191 .body(body)?;
192 Ok(req)
193}
194
195impl<T, C> Query<C> for T
196where
197 T: Endpoint + Send + Sync,
198 C: Client + Send + Sync,
199{
200 type Result = Result<T::Response, QueryError<C::Error>>;
201
202 async fn execute(self, client: &C) -> Self::Result {
203 let method = self.method();
204 let params = self.params();
205 let http_req = build_dispatch_request(method, ¶ms)?;
206
207 let response = client
208 .execute(http_req)
209 .await
210 .map_err(QueryError::transport)?;
211
212 self.parse_response(response.body()).map_err(|e| match e {
213 ParseError::Api { message, kind } => QueryError::Api {
214 message,
215 method,
216 kind,
217 },
218 ParseError::Decode { source, method } => QueryError::Decode { source, method },
219 ParseError::NonJsonBody { body } => QueryError::NonJsonBody { method, body },
220 })
221 }
222}