rest_json_client/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
3use futures_util::{TryFutureExt, future::ready};
4pub use reqwest::Error;
5use reqwest::{Client, RequestBuilder, Response, Result};
6use serde::{Serialize, de::DeserializeOwned};
7
8const DEFAULT_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), " ", env!("CARGO_PKG_VERSION"));
9
10/// Before one can do any api request, an ApiClient must be constructed
11pub struct ApiClient {
12    client: Client,
13    prefix: String,
14    authentication: Authentication,
15}
16
17pub struct ApiClientBuilder {
18    prefix: String,
19    authentication: Authentication,
20    user_agent: Option<String>,
21}
22
23impl ApiClientBuilder {
24    pub fn new(prefix: &str) -> Self {
25        Self {
26            prefix: prefix.to_owned(),
27            authentication: Authentication::default(),
28            user_agent: None,
29        }
30    }
31    pub fn authentication(&mut self, auth: Authentication) -> &mut Self {
32        self.authentication = auth;
33        self
34    }
35    pub fn user_agent(&mut self, user_agent: &str) -> &mut Self {
36        self.user_agent = Some(user_agent.to_owned());
37        self
38    }
39    pub fn build(&self) -> Result<ApiClient> {
40        Client::builder()
41            .user_agent(
42                self.user_agent
43                    .clone()
44                    .unwrap_or(DEFAULT_USER_AGENT.to_owned()),
45            )
46            .build()
47            .map(|client| ApiClient {
48                authentication: self.authentication.clone(),
49                client,
50                prefix: self.prefix.clone(),
51            })
52    }
53}
54/// This library support two ways of authentication
55/// Either Basic of Bearer
56#[derive(Clone, Default)]
57pub enum Authentication {
58    Basic(BasicAuthentication),
59    Bearer(Option<String>),
60    #[default]
61    None,
62}
63
64impl Authentication {
65    pub fn new_basic(username: &str, password: &str) -> Self {
66        Authentication::Basic(BasicAuthentication::new(username, password))
67    }
68    pub fn new_bearer(token: &str) -> Self {
69        Authentication::Bearer(Some(token.to_owned()))
70    }
71}
72
73#[derive(Clone)]
74pub struct BasicAuthentication {
75    username: String,
76    password: String,
77}
78
79impl BasicAuthentication {
80    /// Create a new instance of BasicAuthentication with provided username and password
81    pub fn new<S: Into<String>>(username: S, password: S) -> Self {
82        Self {
83            username: username.into(),
84            password: password.into(),
85        }
86    }
87}
88
89impl ApiClient {
90    fn create_request<T>(
91        &self,
92        uri: &str,
93        f: impl Fn(&Client, String) -> RequestBuilder,
94        t: Option<T>,
95    ) -> RequestBuilder
96    where
97        T: Serialize,
98    {
99        let mut builder = f(&self.client, self.uri(uri));
100        builder = match &self.authentication {
101            Authentication::Basic(basic) => {
102                builder.basic_auth(basic.username.clone(), Some(basic.password.clone()))
103            }
104            Authentication::Bearer(token) => match token {
105                Some(t) => builder.bearer_auth(t.clone()),
106                None => builder,
107            },
108            Authentication::None => builder,
109        };
110
111        if let Some(object) = t {
112            builder = builder.json(&object)
113        }
114
115        builder
116    }
117
118    fn uri(&self, uri: &str) -> String {
119        format!("{}{}", self.prefix, uri)
120    }
121
122    /// # Example
123    ///
124    /// Try to delete a post with specific id from [Json Placeholder](https://jsonplaceholder.typicode.com/)
125    ///
126    /// ```
127    /// # use rest_json_client::{ApiClientBuilder, Authentication, Error};
128    /// # use json_placeholder_data::posts::Post;
129    /// #
130    /// # tokio_test::block_on(async {
131    ///     let base = "https://jsonplaceholder.typicode.com/";
132    ///     ApiClientBuilder::new(base)
133    ///         .build()?
134    ///         .delete("posts/1")
135    ///         .await?;
136    ///
137    /// #       Ok::<(), Error>(())
138    /// # });
139    /// ```
140    pub async fn delete(&self, uri: &str) -> Result<()> {
141        self.create_request::<()>(uri, Client::delete, None)
142            .send()
143            .and_then(|r| ready(r.error_for_status()).map_ok(|_| ()))
144            .await
145    }
146
147    /// # Example 1
148    ///
149    /// Try to return a list of posts from [JsonPlaceholder](https://jsonplaceholder.typicode.com/)
150    ///
151    /// ```rust
152    /// # use rest_json_client::{ApiClientBuilder, Authentication, Error};
153    /// # use json_placeholder_data::posts::Post;
154    /// #
155    /// # tokio_test::block_on(async {
156    ///     let base = "https://jsonplaceholder.typicode.com/";
157    ///     let posts = ApiClientBuilder::new(base)
158    ///         .build()?
159    ///         .get::<Vec<Post>>("posts")
160    ///         .await?;
161    ///
162    /// #       assert_eq!(posts.len(), 100);
163    /// #       Ok::<(), Error>(())
164    /// # });
165    /// ```
166    ///
167    /// # Example 2
168    ///
169    /// Try to return a single post with specific id from [Json Placeholder](https://jsonplaceholder.typicode.com/)
170    ///
171    /// ```rust
172    /// # use rest_json_client::{ApiClientBuilder, Authentication, Error};
173    /// # use json_placeholder_data::posts::Post;
174    /// #
175    /// # tokio_test::block_on(async {
176    ///     let base = "https://jsonplaceholder.typicode.com/";
177    ///     let post = ApiClientBuilder::new(base)
178    ///         .build()?
179    ///         .get::<Post>("posts/1")
180    ///         .await?;
181    ///
182    /// #       assert_eq!(post.user_id, Some(1));
183    /// #       Ok::<(), Error>(())
184    /// # });
185    /// ```
186    pub async fn get<R>(&self, uri: &str) -> Result<R>
187    where
188        R: DeserializeOwned,
189    {
190        self.create_request::<()>(uri, Client::get, None)
191            .send()
192            .and_then(Response::json::<R>)
193            .await
194    }
195
196    /// # Example
197    ///
198    /// Try to create a new post on [Json Placeholder](https://jsonplaceholder.typicode.com/)
199    /// If successful the created post is returned
200    ///
201    /// ```
202    /// # use rest_json_client::{ApiClientBuilder, Authentication, Error};
203    /// # use json_placeholder_data::posts::Post;
204    /// #
205    /// # tokio_test::block_on(async {
206    ///
207    ///     let new_post = Post {
208    ///         id: None,
209    ///         title: "Hallo".to_owned(),
210    ///         body: "Hallo".to_owned(),
211    ///         user_id: Some(34),
212    ///     };
213    ///     let base = "https://jsonplaceholder.typicode.com/";
214    ///     let post = ApiClientBuilder::new(base)
215    ///         .build()?
216    ///         .post::<_, Post>("posts", new_post)
217    ///         .await?;
218    ///
219    /// #     assert_eq!(post.user_id, Some(34));
220    /// #     Ok::<(), Error>(())
221    /// # });
222    /// ```
223    pub async fn post<T, R>(&self, uri: &str, object: T) -> Result<R>
224    where
225        T: Serialize,
226        R: DeserializeOwned,
227    {
228        self.create_request::<T>(uri, Client::post, Some(object))
229            .send()
230            .and_then(Response::json::<R>)
231            .await
232    }
233
234    /// use post_validation to get a Json Web Token
235    pub async fn token_request<T>(&mut self, uri: &str, signature: &str, object: T) -> Result<()>
236    where
237        T: Serialize,
238    {
239        let token = self
240            .create_request::<T>(uri, Client::post, Some(object))
241            .header("Signature", signature)
242            .send()
243            .and_then(Response::text)
244            .await?;
245        self.authentication = Authentication::Bearer(Some(token));
246        Ok(())
247    }
248
249    /// # Example
250    ///
251    /// Try to change a post with specific id on [Json Placeholder](https://jsonplaceholder.typicode.com/)
252    /// If successful the changed post is returned
253    ///
254    /// ```
255    /// # use rest_json_client::{ApiClientBuilder, Authentication, Error};
256    /// # use json_placeholder_data::posts::Post;
257    /// #
258    /// # tokio_test::block_on(async {
259    ///
260    ///     let changed_post = Post {
261    ///         id: None,
262    ///         title: "Hallo".to_owned(),
263    ///         body: "Hallo".to_owned(),
264    ///         user_id: Some(34),
265    ///     };
266    ///     let base = "https://jsonplaceholder.typicode.com/";
267    ///     let post = ApiClientBuilder::new(base)
268    ///         .build()?
269    ///         .put::<_, Post>("posts/1", changed_post)
270    ///         .await?;
271    ///
272    /// #     assert_eq!(post.user_id, Some(34));
273    /// #     Ok::<(), Error>(())
274    /// # });
275    /// ```
276    pub async fn put<T, R>(&self, uri: &str, object: T) -> Result<R>
277    where
278        T: Serialize,
279        R: DeserializeOwned,
280    {
281        self.create_request::<T>(uri, Client::put, Some(object))
282            .send()
283            .and_then(Response::json::<R>)
284            .await
285    }
286}