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}