clevercloud_sdk/
lib.rs

1//! # Clever-Cloud Sdk
2//!
3//! This module provides a client and structures to interact with clever-cloud
4//! api.
5
6use std::fmt::Debug;
7
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize, de::DeserializeOwned};
10
11use crate::oauth10a::{
12    Client as OAuthClient, ClientError, Request, RestClient,
13    reqwest::{self, Method},
14};
15
16pub mod v2;
17pub mod v4;
18
19// -----------------------------------------------------------------------------
20// Exports
21
22pub use oauth10a::client as oauth10a;
23
24// -----------------------------------------------------------------------------
25// Constants
26
27pub const PUBLIC_ENDPOINT: &str = "https://api.clever-cloud.com";
28pub const PUBLIC_API_BRIDGE_ENDPOINT: &str = "https://api-bridge.clever-cloud.com";
29
30// Consumer key and secret reported here are one from the clever-tools and is
31// available publicly.
32// the disclosure of these tokens is not considered as a vulnerability.
33// Do not report this to our security service.
34//
35// See:
36// - <https://github.com/CleverCloud/clever-tools/blob/fed085e2ba0339f55e966d7c8c6439d4dac71164/src/models/configuration.js#L128>
37pub const DEFAULT_CONSUMER_KEY: &str = "T5nFjKeHH4AIlEveuGhB5S3xg8T19e";
38pub fn default_consumer_key() -> String {
39    DEFAULT_CONSUMER_KEY.to_string()
40}
41
42pub const DEFAULT_CONSUMER_SECRET: &str = "MgVMqTr6fWlf2M0tkC2MXOnhfqBWDT";
43pub fn default_consumer_secret() -> String {
44    DEFAULT_CONSUMER_SECRET.to_string()
45}
46
47// -----------------------------------------------------------------------------
48// Credentials structure
49
50#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)]
51#[serde(untagged)]
52pub enum Credentials {
53    OAuth1 {
54        #[serde(rename = "token")]
55        token: String,
56        #[serde(rename = "secret")]
57        secret: String,
58        #[serde(rename = "consumer-key", default = "default_consumer_key")]
59        consumer_key: String,
60        #[serde(rename = "consumer-secret", default = "default_consumer_secret")]
61        consumer_secret: String,
62    },
63    Basic {
64        #[serde(rename = "username")]
65        username: String,
66        #[serde(rename = "password")]
67        password: String,
68    },
69    Bearer {
70        #[serde(rename = "token")]
71        token: String,
72    },
73}
74
75impl Default for Credentials {
76    #[tracing::instrument(skip_all)]
77    fn default() -> Self {
78        Self::OAuth1 {
79            token: String::new(),
80            secret: String::new(),
81            consumer_key: DEFAULT_CONSUMER_KEY.to_string(),
82            consumer_secret: DEFAULT_CONSUMER_SECRET.to_string(),
83        }
84    }
85}
86
87impl From<oauth10a::Credentials> for Credentials {
88    #[tracing::instrument(skip_all)]
89    fn from(credentials: oauth10a::Credentials) -> Self {
90        match credentials {
91            oauth10a::Credentials::Bearer { token } => Self::Bearer { token },
92            oauth10a::Credentials::Basic { username, password } => {
93                Self::Basic { username, password }
94            }
95            oauth10a::Credentials::OAuth1 {
96                token,
97                secret,
98                consumer_key,
99                consumer_secret,
100            } => Self::OAuth1 {
101                token,
102                secret,
103                consumer_key,
104                consumer_secret,
105            },
106        }
107    }
108}
109
110#[allow(clippy::from_over_into)]
111impl Into<oauth10a::Credentials> for Credentials {
112    #[tracing::instrument(skip_all)]
113    fn into(self) -> oauth10a::Credentials {
114        match self {
115            Self::Bearer { token } => oauth10a::Credentials::Bearer { token },
116            Self::Basic { username, password } => {
117                oauth10a::Credentials::Basic { username, password }
118            }
119            Self::OAuth1 {
120                token,
121                secret,
122                consumer_key,
123                consumer_secret,
124            } => oauth10a::Credentials::OAuth1 {
125                token,
126                secret,
127                consumer_key,
128                consumer_secret,
129            },
130        }
131    }
132}
133
134impl Credentials {
135    #[tracing::instrument(skip_all)]
136    pub fn bearer(token: String) -> Self {
137        Self::Bearer { token }
138    }
139
140    #[tracing::instrument(skip_all)]
141    pub fn basic(username: String, password: String) -> Self {
142        Self::Basic { username, password }
143    }
144
145    #[tracing::instrument(skip_all)]
146    pub fn oauth1(
147        token: String,
148        secret: String,
149        consumer_key: String,
150        consumer_secret: String,
151    ) -> Self {
152        Self::OAuth1 {
153            token,
154            secret,
155            consumer_key,
156            consumer_secret,
157        }
158    }
159}
160
161// -----------------------------------------------------------------------------
162// Builder structure
163
164#[derive(Clone, Debug, Default)]
165pub struct Builder {
166    endpoint: Option<String>,
167    credentials: Option<Credentials>,
168}
169
170impl Builder {
171    #[cfg_attr(feature = "tracing", tracing::instrument)]
172    pub fn with_endpoint(mut self, endpoint: String) -> Self {
173        self.endpoint = Some(endpoint);
174        self
175    }
176
177    #[cfg_attr(feature = "tracing", tracing::instrument)]
178    pub fn with_credentials(mut self, credentials: Credentials) -> Self {
179        self.credentials = Some(credentials);
180        self
181    }
182
183    #[cfg_attr(feature = "tracing", tracing::instrument)]
184    pub fn build(self, client: reqwest::Client) -> Client {
185        let endpoint = match self.endpoint {
186            Some(endpoint) => endpoint,
187            None => {
188                if matches!(self.credentials, Some(Credentials::Bearer { .. })) {
189                    PUBLIC_API_BRIDGE_ENDPOINT.to_string()
190                } else {
191                    PUBLIC_ENDPOINT.to_string()
192                }
193            }
194        };
195
196        Client {
197            inner: OAuthClient::new(client, self.credentials.map(Into::into)),
198            endpoint,
199        }
200    }
201}
202
203// -----------------------------------------------------------------------------
204// Client structure
205
206#[derive(Clone, Debug)]
207pub struct Client {
208    inner: OAuthClient,
209    endpoint: String,
210}
211
212#[async_trait]
213impl Request for Client {
214    type Error = ClientError;
215
216    #[cfg_attr(feature = "tracing", tracing::instrument)]
217    async fn request<T, U>(
218        &self,
219        method: &Method,
220        endpoint: &str,
221        payload: &T,
222    ) -> Result<U, Self::Error>
223    where
224        T: Serialize + Debug + Send + Sync,
225        U: DeserializeOwned + Debug + Send + Sync,
226    {
227        self.inner.request(method, endpoint, payload).await
228    }
229
230    #[cfg_attr(feature = "tracing", tracing::instrument)]
231    async fn execute(&self, request: reqwest::Request) -> Result<reqwest::Response, Self::Error> {
232        self.inner.execute(request).await
233    }
234}
235
236#[async_trait]
237impl RestClient for Client {
238    type Error = ClientError;
239
240    #[cfg_attr(feature = "tracing", tracing::instrument)]
241    async fn get<T>(&self, endpoint: &str) -> Result<T, Self::Error>
242    where
243        T: DeserializeOwned + Debug + Send + Sync,
244    {
245        self.inner.get(endpoint).await
246    }
247
248    #[cfg_attr(feature = "tracing", tracing::instrument)]
249    async fn post<T, U>(&self, endpoint: &str, payload: &T) -> Result<U, Self::Error>
250    where
251        T: Serialize + Debug + Send + Sync,
252        U: DeserializeOwned + Debug + Send + Sync,
253    {
254        self.inner.post(endpoint, payload).await
255    }
256
257    #[cfg_attr(feature = "tracing", tracing::instrument)]
258    async fn put<T, U>(&self, endpoint: &str, payload: &T) -> Result<U, Self::Error>
259    where
260        T: Serialize + Debug + Send + Sync,
261        U: DeserializeOwned + Debug + Send + Sync,
262    {
263        self.inner.put(endpoint, payload).await
264    }
265
266    #[cfg_attr(feature = "tracing", tracing::instrument)]
267    async fn patch<T, U>(&self, endpoint: &str, payload: &T) -> Result<U, Self::Error>
268    where
269        T: Serialize + Debug + Send + Sync,
270        U: DeserializeOwned + Debug + Send + Sync,
271    {
272        self.inner.patch(endpoint, payload).await
273    }
274
275    #[cfg_attr(feature = "tracing", tracing::instrument)]
276    async fn delete(&self, endpoint: &str) -> Result<(), Self::Error> {
277        self.inner.delete(endpoint).await
278    }
279}
280
281impl From<reqwest::Client> for Client {
282    #[cfg_attr(feature = "tracing", tracing::instrument)]
283    fn from(client: reqwest::Client) -> Self {
284        Self::builder().build(client)
285    }
286}
287
288impl From<Credentials> for Client {
289    #[cfg_attr(feature = "tracing", tracing::instrument)]
290    fn from(credentials: Credentials) -> Self {
291        match &credentials {
292            Credentials::Bearer { .. } => Self::builder()
293                .with_endpoint(PUBLIC_API_BRIDGE_ENDPOINT.to_string())
294                .with_credentials(credentials)
295                .build(reqwest::Client::new()),
296            _ => Self::builder()
297                .with_credentials(credentials)
298                .build(reqwest::Client::new()),
299        }
300    }
301}
302
303impl Default for Client {
304    #[cfg_attr(feature = "tracing", tracing::instrument)]
305    fn default() -> Self {
306        Self::builder().build(reqwest::Client::new())
307    }
308}
309
310impl Client {
311    #[cfg_attr(feature = "tracing", tracing::instrument)]
312    pub fn new(
313        client: reqwest::Client,
314        endpoint: String,
315        credentials: Option<Credentials>,
316    ) -> Self {
317        let mut builder = Self::builder().with_endpoint(endpoint);
318
319        if let Some(credentials) = credentials {
320            builder = builder.with_credentials(credentials);
321        }
322
323        builder.build(client)
324    }
325
326    #[cfg_attr(feature = "tracing", tracing::instrument)]
327    pub fn builder() -> Builder {
328        Builder::default()
329    }
330
331    #[cfg_attr(feature = "tracing", tracing::instrument)]
332    pub fn set_endpoint(&mut self, endpoint: String) {
333        self.endpoint = endpoint;
334    }
335
336    #[cfg_attr(feature = "tracing", tracing::instrument)]
337    pub fn set_credentials(&mut self, credentials: Option<Credentials>) {
338        self.inner.set_credentials(credentials.map(Into::into));
339    }
340
341    #[cfg_attr(feature = "tracing", tracing::instrument)]
342    pub fn inner(&self) -> &reqwest::Client {
343        self.inner.inner()
344    }
345}