1use std::{fmt::Debug, future::Future};
7
8use ::oauth10a::{
9 client::{Client as OAuthClient, ClientError as OAuthClientError},
10 credentials::Credentials as OAuthCredentials,
11 execute::ExecuteRequest,
12 reqwest::{self, Method, StatusCode},
13 rest::{ErrorResponse, RestClient as OAuthRestClient, RestError},
14};
15use serde::{Deserialize, Serialize, de::DeserializeOwned};
16
17pub mod v2;
18pub mod v4;
19
20pub mod oauth10a {
34 pub use ::oauth10a::{reqwest, url};
35
36 pub use crate::{ClientError, Request, ResponseError, RestClient};
37}
38
39pub const PUBLIC_ENDPOINT: &str = "https://api.clever-cloud.com";
43pub const PUBLIC_API_BRIDGE_ENDPOINT: &str = "https://api-bridge.clever-cloud.com";
44
45pub const DEFAULT_CONSUMER_KEY: &str = "T5nFjKeHH4AIlEveuGhB5S3xg8T19e";
53pub fn default_consumer_key() -> String {
54 DEFAULT_CONSUMER_KEY.to_string()
55}
56
57pub const DEFAULT_CONSUMER_SECRET: &str = "MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT";
58pub fn default_consumer_secret() -> String {
59 DEFAULT_CONSUMER_SECRET.to_string()
60}
61
62pub type ResponseError = serde_json::Value;
67
68#[derive(thiserror::Error, Debug)]
72pub enum ClientError {
73 #[error("failed to execute request, {0}")]
74 Request(reqwest::Error),
75 #[error("failed to authorize or execute request, {0}")]
76 Execute(String),
77 #[error("failed to execute request, got status code {0}, {1}")]
78 StatusCode(StatusCode, ResponseError),
79 #[error("failed to serialize request body, {0}")]
80 Serialize(serde_json::Error),
81 #[error("failed to deserialize response body, {0}")]
82 Deserialize(serde_json::Error),
83}
84
85impl From<RestError<OAuthClientError>> for ClientError {
86 fn from(err: RestError<OAuthClientError>) -> Self {
87 match err {
88 RestError::Execute(err) => Self::Execute(err.to_string()),
89 RestError::Serialize(err) => Self::Serialize(err),
90 RestError::Deserialize(err) => Self::Deserialize(err),
91 RestError::Url(err) | RestError::BodyAggregation(err) => Self::Request(err),
92 RestError::TooManyHeaders(err) => Self::Execute(err.to_string()),
93 }
94 }
95}
96
97impl From<OAuthClientError> for ClientError {
98 fn from(err: OAuthClientError) -> Self {
99 Self::Execute(err.to_string())
100 }
101}
102
103pub trait Request {
108 type Error;
109
110 fn request<T, U>(
111 &self,
112 method: &Method,
113 endpoint: &str,
114 payload: &T,
115 ) -> impl Future<Output = Result<U, Self::Error>> + Send
116 where
117 T: ?Sized + Serialize + Debug + Send + Sync,
118 U: DeserializeOwned + Debug + Send + Sync;
119
120 fn execute(
121 &self,
122 request: reqwest::Request,
123 ) -> impl Future<Output = Result<reqwest::Response, Self::Error>> + Send;
124}
125
126pub trait RestClient {
128 type Error;
129
130 fn get<T>(&self, endpoint: &str) -> impl Future<Output = Result<T, Self::Error>> + Send
131 where
132 T: DeserializeOwned + Debug + Send + Sync;
133
134 fn post<T, U>(
135 &self,
136 endpoint: &str,
137 payload: &T,
138 ) -> impl Future<Output = Result<U, Self::Error>> + Send
139 where
140 T: ?Sized + Serialize + Debug + Send + Sync,
141 U: DeserializeOwned + Debug + Send + Sync;
142
143 fn put<T, U>(
144 &self,
145 endpoint: &str,
146 payload: &T,
147 ) -> impl Future<Output = Result<U, Self::Error>> + Send
148 where
149 T: ?Sized + Serialize + Debug + Send + Sync,
150 U: DeserializeOwned + Debug + Send + Sync;
151
152 fn patch<T, U>(
153 &self,
154 endpoint: &str,
155 payload: &T,
156 ) -> impl Future<Output = Result<U, Self::Error>> + Send
157 where
158 T: ?Sized + Serialize + Debug + Send + Sync,
159 U: DeserializeOwned + Debug + Send + Sync;
160
161 fn delete(&self, endpoint: &str) -> impl Future<Output = Result<(), Self::Error>> + Send;
162}
163
164#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
168#[serde(untagged)]
169pub enum Credentials {
170 OAuth1 {
171 #[serde(rename = "token")]
172 token: String,
173 #[serde(rename = "secret")]
174 secret: String,
175 #[serde(rename = "consumer-key", default = "default_consumer_key")]
176 consumer_key: String,
177 #[serde(rename = "consumer-secret", default = "default_consumer_secret")]
178 consumer_secret: String,
179 },
180 Basic {
181 #[serde(rename = "username")]
182 username: String,
183 #[serde(rename = "password")]
184 password: String,
185 },
186 Bearer {
187 #[serde(rename = "token")]
188 token: String,
189 },
190}
191
192impl Default for Credentials {
193 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
194 fn default() -> Self {
195 Self::OAuth1 {
196 token: String::new(),
197 secret: String::new(),
198 consumer_key: DEFAULT_CONSUMER_KEY.to_string(),
199 consumer_secret: DEFAULT_CONSUMER_SECRET.to_string(),
200 }
201 }
202}
203
204impl From<OAuthCredentials> for Credentials {
205 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
206 fn from(credentials: OAuthCredentials) -> Self {
207 match credentials {
208 OAuthCredentials::Bearer { token } => Self::Bearer {
209 token: token.into(),
210 },
211 OAuthCredentials::Basic { username, password } => Self::Basic {
212 username: username.into(),
213 password: password.map(Into::into).unwrap_or_default(),
214 },
215 OAuthCredentials::OAuth1 {
216 token,
217 secret,
218 consumer_key,
219 consumer_secret,
220 } => Self::OAuth1 {
221 token: token.into(),
222 secret: secret.into(),
223 consumer_key: consumer_key.into(),
224 consumer_secret: consumer_secret.into(),
225 },
226 }
227 }
228}
229
230impl From<Credentials> for OAuthCredentials {
231 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
232 fn from(credentials: Credentials) -> Self {
233 match credentials {
234 Credentials::Bearer { token } => Self::Bearer {
235 token: token.into(),
236 },
237 Credentials::Basic { username, password } => Self::Basic {
238 username: username.into(),
239 password: Some(password.into()),
240 },
241 Credentials::OAuth1 {
242 token,
243 secret,
244 consumer_key,
245 consumer_secret,
246 } => Self::OAuth1 {
247 token: token.into(),
248 secret: secret.into(),
249 consumer_key: consumer_key.into(),
250 consumer_secret: consumer_secret.into(),
251 },
252 }
253 }
254}
255
256impl Credentials {
257 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
258 pub fn bearer(token: String) -> Self {
259 Self::Bearer { token }
260 }
261
262 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
263 pub fn basic(username: String, password: String) -> Self {
264 Self::Basic { username, password }
265 }
266
267 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
268 pub fn oauth1(
269 token: String,
270 secret: String,
271 consumer_key: String,
272 consumer_secret: String,
273 ) -> Self {
274 Self::OAuth1 {
275 token,
276 secret,
277 consumer_key,
278 consumer_secret,
279 }
280 }
281}
282
283#[derive(Clone, Debug, Default)]
287pub struct Builder {
288 endpoint: Option<String>,
289 credentials: Option<Credentials>,
290}
291
292impl Builder {
293 #[cfg_attr(feature = "tracing", tracing::instrument)]
294 pub fn with_endpoint(mut self, endpoint: String) -> Self {
295 self.endpoint = Some(endpoint);
296 self
297 }
298
299 #[cfg_attr(feature = "tracing", tracing::instrument)]
300 pub fn with_credentials(mut self, credentials: Credentials) -> Self {
301 self.credentials = Some(credentials);
302 self
303 }
304
305 #[cfg_attr(feature = "tracing", tracing::instrument)]
306 pub fn build(self, client: reqwest::Client) -> Client {
307 let endpoint = match self.endpoint {
308 Some(endpoint) => endpoint,
309 None => {
310 if matches!(self.credentials, Some(Credentials::Bearer { .. })) {
311 PUBLIC_API_BRIDGE_ENDPOINT.to_string()
312 } else {
313 PUBLIC_ENDPOINT.to_string()
314 }
315 }
316 };
317
318 let inner = OAuthClient::from(client)
319 .with_credentials(self.credentials.map(Into::<OAuthCredentials>::into));
320
321 Client { inner, endpoint }
322 }
323}
324
325#[derive(Clone, Debug)]
329pub struct Client {
330 inner: OAuthClient,
331 endpoint: String,
332}
333
334impl Request for Client {
335 type Error = ClientError;
336
337 #[cfg_attr(feature = "tracing", tracing::instrument)]
338 fn request<T, U>(
339 &self,
340 method: &Method,
341 endpoint: &str,
342 payload: &T,
343 ) -> impl Future<Output = Result<U, Self::Error>> + Send
344 where
345 T: ?Sized + Serialize + Debug + Send + Sync,
346 U: DeserializeOwned + Debug + Send + Sync,
347 {
348 let fut =
349 OAuthRestClient::request::<T, U, ResponseError>(&self.inner, method, endpoint, payload);
350 async move { flatten(fut.await) }
351 }
352
353 #[cfg_attr(feature = "tracing", tracing::instrument(skip(request)))]
354 fn execute(
355 &self,
356 request: reqwest::Request,
357 ) -> impl Future<Output = Result<reqwest::Response, Self::Error>> + Send {
358 let fut = self.inner.execute_request(request);
359 async move { fut.await.map_err(ClientError::from) }
360 }
361}
362
363impl RestClient for Client {
364 type Error = ClientError;
365
366 #[cfg_attr(feature = "tracing", tracing::instrument)]
367 fn get<T>(&self, endpoint: &str) -> impl Future<Output = Result<T, Self::Error>> + Send
368 where
369 T: DeserializeOwned + Debug + Send + Sync,
370 {
371 let fut = OAuthRestClient::get::<T, ResponseError>(&self.inner, endpoint);
372 async move { flatten(fut.await) }
373 }
374
375 #[cfg_attr(feature = "tracing", tracing::instrument)]
376 fn post<T, U>(
377 &self,
378 endpoint: &str,
379 payload: &T,
380 ) -> impl Future<Output = Result<U, Self::Error>> + Send
381 where
382 T: ?Sized + Serialize + Debug + Send + Sync,
383 U: DeserializeOwned + Debug + Send + Sync,
384 {
385 let fut = OAuthRestClient::post::<T, U, ResponseError>(&self.inner, endpoint, payload);
386 async move { flatten(fut.await) }
387 }
388
389 #[cfg_attr(feature = "tracing", tracing::instrument)]
390 fn put<T, U>(
391 &self,
392 endpoint: &str,
393 payload: &T,
394 ) -> impl Future<Output = Result<U, Self::Error>> + Send
395 where
396 T: ?Sized + Serialize + Debug + Send + Sync,
397 U: DeserializeOwned + Debug + Send + Sync,
398 {
399 let fut = OAuthRestClient::put::<T, U, ResponseError>(&self.inner, endpoint, payload);
400 async move { flatten(fut.await) }
401 }
402
403 #[cfg_attr(feature = "tracing", tracing::instrument)]
404 fn patch<T, U>(
405 &self,
406 endpoint: &str,
407 payload: &T,
408 ) -> impl Future<Output = Result<U, Self::Error>> + Send
409 where
410 T: ?Sized + Serialize + Debug + Send + Sync,
411 U: DeserializeOwned + Debug + Send + Sync,
412 {
413 let fut = OAuthRestClient::patch::<T, U, ResponseError>(&self.inner, endpoint, payload);
414 async move { flatten(fut.await) }
415 }
416
417 #[cfg_attr(feature = "tracing", tracing::instrument)]
418 fn delete(&self, endpoint: &str) -> impl Future<Output = Result<(), Self::Error>> + Send {
419 let fut = OAuthRestClient::delete::<ResponseError>(&self.inner, endpoint);
420 async move { flatten(fut.await) }
421 }
422}
423
424fn flatten<T>(
427 result: Result<Result<T, ErrorResponse<ResponseError>>, RestError<OAuthClientError>>,
428) -> Result<T, ClientError> {
429 match result {
430 Ok(Ok(value)) => Ok(value),
431 Ok(Err(ErrorResponse { status_code, value })) => {
432 Err(ClientError::StatusCode(status_code, value))
433 }
434 Err(err) => Err(ClientError::from(err)),
435 }
436}
437
438impl From<reqwest::Client> for Client {
439 #[cfg_attr(feature = "tracing", tracing::instrument)]
440 fn from(client: reqwest::Client) -> Self {
441 Self::builder().build(client)
442 }
443}
444
445impl From<Credentials> for Client {
446 #[cfg_attr(feature = "tracing", tracing::instrument)]
447 fn from(credentials: Credentials) -> Self {
448 match &credentials {
449 Credentials::Bearer { .. } => Self::builder()
450 .with_endpoint(PUBLIC_API_BRIDGE_ENDPOINT.to_string())
451 .with_credentials(credentials)
452 .build(reqwest::Client::new()),
453 _ => Self::builder()
454 .with_credentials(credentials)
455 .build(reqwest::Client::new()),
456 }
457 }
458}
459
460impl Default for Client {
461 #[cfg_attr(feature = "tracing", tracing::instrument)]
462 fn default() -> Self {
463 Self::builder().build(reqwest::Client::new())
464 }
465}
466
467impl Client {
468 #[cfg_attr(feature = "tracing", tracing::instrument)]
469 pub fn new(
470 client: reqwest::Client,
471 endpoint: String,
472 credentials: Option<Credentials>,
473 ) -> Self {
474 let mut builder = Self::builder().with_endpoint(endpoint);
475
476 if let Some(credentials) = credentials {
477 builder = builder.with_credentials(credentials);
478 }
479
480 builder.build(client)
481 }
482
483 #[cfg_attr(feature = "tracing", tracing::instrument)]
484 pub fn builder() -> Builder {
485 Builder::default()
486 }
487
488 #[cfg_attr(feature = "tracing", tracing::instrument)]
489 pub fn set_endpoint(&mut self, endpoint: String) {
490 self.endpoint = endpoint;
491 }
492
493 #[cfg_attr(feature = "tracing", tracing::instrument)]
494 pub fn set_credentials(&mut self, credentials: Option<Credentials>) {
495 self.inner
496 .set_credentials(credentials.map(Into::<OAuthCredentials>::into));
497 }
498
499 #[cfg_attr(feature = "tracing", tracing::instrument)]
500 pub fn inner(&self) -> &reqwest::Client {
501 self.inner.inner()
502 }
503}
504
505#[cfg(test)]
509mod tests {
510 use serde_json::json;
511
512 use super::*;
513
514 #[test]
522 fn flatten_maps_error_response_to_status_code() {
523 let body = json!({ "id": 4004, "message": "addon not found" });
524 let result: Result<Result<(), ErrorResponse<ResponseError>>, RestError<OAuthClientError>> =
525 Ok(Err(ErrorResponse {
526 status_code: StatusCode::NOT_FOUND,
527 value: body.clone(),
528 }));
529
530 match flatten(result) {
531 Err(ClientError::StatusCode(status, value)) => {
532 assert_eq!(status, StatusCode::NOT_FOUND);
533 assert_eq!(value, body);
534 }
535 other => panic!("expected ClientError::StatusCode(404, _), got {other:?}"),
536 }
537 }
538
539 #[test]
541 fn flatten_forwards_success() {
542 let result: Result<Result<u32, ErrorResponse<ResponseError>>, RestError<OAuthClientError>> =
543 Ok(Ok(42));
544
545 assert!(matches!(flatten(result), Ok(42)));
546 }
547
548 #[test]
552 fn flatten_keeps_transport_errors_distinct_from_status_code() {
553 let serde_err = serde_json::from_str::<u32>("not-a-number").unwrap_err();
554 let result: Result<Result<(), ErrorResponse<ResponseError>>, RestError<OAuthClientError>> =
555 Err(RestError::Serialize(serde_err));
556
557 assert!(matches!(flatten(result), Err(ClientError::Serialize(_))));
558 }
559}