codewars_api/rest_api/client.rs
1//! Client for interacting with the Codewars REST API
2
3use std::string::ToString;
4use crate::rest_api::models::{AuthoredChallenges, CodeChallenge, CompletedChallenges, User};
5
6/// Client for interacting with the Codewars API
7#[derive(Clone)]
8pub struct RestCodewarsClient {
9 host_name: String
10}
11
12/// Implementation of RestCodewarsClient
13impl RestCodewarsClient {
14 /// Create new instance of RestCodewarsClient
15 ///
16 /// # Examples
17 ///
18 /// ```
19 /// use codewars_api::rest_api::client::RestCodewarsClient;
20 ///
21 /// // We use Tokio here, because all functions of client is asynchronous
22 /// #[tokio::main]
23 /// async fn main() {
24 /// let client = RestCodewarsClient::new();
25 /// // We can use methods of client here
26 /// }
27 /// ```
28 pub fn new() -> Self {
29 Self {
30 host_name: "https://www.codewars.com".to_string()
31 }
32 }
33
34 /// Create new instance of RestCodewarsClient with custom host name
35 /// Currently this used for unit tests
36 #[doc(hidden)]
37 pub(crate) fn new_with_custom_host(host_name: String) -> Self {
38 Self {
39 host_name
40 }
41 }
42
43 /// Get info about user by username
44 ///
45 /// # Arguments:
46 /// * username (&str) - username of the user
47 ///
48 /// # Returns:
49 /// * Result<User, String> - Result that contains the user or an error message
50 ///
51 /// # Errors:
52 /// * `unexpected status code: {status_code}` - If the status code is not 200
53 /// * `error decoding response body` - If there is an error decoding the response body with serde
54 ///
55 /// # Examples
56 /// ```no_run
57 /// # use codewars_api::rest_api::client::RestCodewarsClient;
58 /// # #[tokio::main]
59 /// # async fn main() {
60 /// # let client = RestCodewarsClient::new();
61 /// let user = client.get_user("ANKDDEV").await.unwrap();
62 /// // Get name of user
63 /// println!("Name: {}", user.name);
64 /// // Get leaderboard position of user
65 /// println!("Leaderboard position: {}", user.leaderboard_position);
66 /// # }
67 /// ```
68 pub async fn get_user(&self, username: &str) -> Result<User, String> {
69 // Send request
70 let response = reqwest::get(format!(
71 "{}/api/v1/users/{}",
72 self.host_name, username
73 ))
74 .await
75 .unwrap();
76 // Check status code
77 match response.status() {
78 reqwest::StatusCode::OK => match response.json::<User>().await {
79 // Return parsed response
80 Ok(parsed) => Ok(parsed),
81 // Return error if there is an error decoding the response body with serde
82 Err(err) => Err(err.to_string()),
83 },
84 // Return error if status code is not 200
85 other => Err(format!("unexpected status code: {}", other)),
86 }
87 }
88
89 /// Get info about kata by slug
90 ///
91 /// # Arguments:
92 /// * slug (&str) - slug of the kata
93 ///
94 /// # Returns:
95 /// * Result<CodeChallenge, String> - Result that contains the kata or an error message
96 ///
97 /// # Errors:
98 /// * `unexpected status code: {status_code}` - If the status code is not 200
99 /// * `error decoding response body` - If there is an error decoding the response body with serde
100 ///
101 /// # Examples
102 /// ```no_run
103 /// # use codewars_api::rest_api::client::RestCodewarsClient;
104 /// # #[tokio::main]
105 /// # async fn main() {
106 /// # let client = RestCodewarsClient::new();
107 /// let kata = client.get_kata("576bb71bbbcf0951d5000044").await.unwrap();
108 /// // Get name of code challenge
109 /// println!("Name: {}", kata.name);
110 /// // Get slug of code challenge
111 /// println!("Slug: {}", kata.slug);
112 /// # }
113 /// ```
114 pub async fn get_kata(&self, slug: &str) -> Result<CodeChallenge, String> {
115 // Send request
116 let response = reqwest::get(format!(
117 "{}/api/v1/code-challenges/{}",
118 self.host_name, slug
119 ))
120 .await
121 .unwrap();
122 // Check status code
123 match response.status() {
124 reqwest::StatusCode::OK => match response.json::<CodeChallenge>().await {
125 // Return parsed response
126 Ok(parsed) => Ok(parsed),
127 // Return error if there is an error decoding the response body with serde
128 Err(err) => Err(err.to_string()),
129 },
130 // Return error if status code is not 200
131 other => Err(format!("unexpected status code: {}", other)),
132 }
133 }
134
135 /// Get list of completed challenges
136 ///
137 /// # Arguments:
138 /// * username (&str) - username of the user
139 /// * page (u16) - page number
140 ///
141 /// # Returns:
142 /// * Result<CompletedChallenges, String> - Result that contains the list of completed challenges or an error message
143 ///
144 /// # Errors:
145 /// * `unexpected status code: {status_code}` - If the status code is not 200
146 /// * `error decoding response body` - If there is an error decoding the response body with serde///
147 ///
148 /// # Examples
149 /// ```no_run
150 /// # use codewars_api::rest_api::client::RestCodewarsClient;
151 /// # #[tokio::main]
152 /// # async fn main() {
153 /// # let client = RestCodewarsClient::new();
154 /// let challenges = client.get_completed_challenges("ANKDDEV", 0).await.unwrap();
155 /// // Get total number of pages
156 /// println!("Total pages: {}", challenges.total_pages);
157 /// // Get total number of items
158 /// println!("Total items: {}", challenges.total_items);
159 /// # }
160 /// ```
161 pub async fn get_completed_challenges(
162 &self,
163 username: &str,
164 page: u16,
165 ) -> Result<CompletedChallenges, String> {
166 // Send request
167 let response = reqwest::get(format!(
168 "{}/api/v1/users/{}/code-challenges/completed?page={}",
169 self.host_name, username, page
170 ))
171 .await
172 .unwrap();
173 // Check status code
174 match response.status() {
175 reqwest::StatusCode::OK => match response.json::<CompletedChallenges>().await {
176 // Return parsed response
177 Ok(parsed) => Ok(parsed),
178 // Return error if there is an error decoding the response body with serde
179 Err(err) => Err(err.to_string()),
180 },
181 // Return error if status code is not 200
182 other => Err(format!("unexpected status code: {}", other)),
183 }
184 }
185
186 /// Get first page of completed challenges
187 ///
188 /// # Arguments:
189 /// * username (&str) - username of the user
190 ///
191 /// # Returns:
192 /// * Result<CompletedChallenges, String> - Result that contains the list of completed challenges or an error message
193 ///
194 /// # Errors:
195 /// * `unexpected status code: {status_code}` - If the status code is not 200
196 /// * `error decoding response body` - If there is an error decoding the response body with serde
197 ///
198 /// # Examples
199 /// ```no_run
200 /// # use codewars_api::rest_api::client::RestCodewarsClient;
201 /// # #[tokio::main]
202 /// # async fn main() {
203 /// # let client = RestCodewarsClient::new();
204 /// let challenges = client.get_completed_challenges_first_page("ANKDDEV").await.unwrap();
205 /// // Get total number of pages
206 /// println!("Total pages: {}", challenges.total_pages);
207 /// // Get total number of items
208 /// println!("Total items: {}", challenges.total_items);
209 /// # }
210 /// ```
211 pub async fn get_completed_challenges_first_page(
212 &self,
213 username: &str,
214 ) -> Result<CompletedChallenges, String> {
215 // Return first page of list
216 self.get_completed_challenges(username, 0).await
217 }
218
219 /// Get list of authored challenges
220 ///
221 /// # Arguments:
222 /// * username (&str) - username of the user
223 ///
224 /// # Returns:
225 /// * Result<AuthoredChallenges, String> - Result that contains the list of authored challenges or an error message
226 ///
227 /// # Errors:
228 /// * `unexpected status code: {status_code}` - If the status code is not 200
229 /// * `error decoding response body` - If there is an error decoding the response body with serde
230 ///
231 /// # Examples
232 /// ```no_run
233 /// # use codewars_api::rest_api::client::RestCodewarsClient;
234 /// # #[tokio::main]
235 /// # async fn main() {
236 /// # let client = RestCodewarsClient::new();
237 /// let challenges = client.get_authored_challenges("Dentzil").await.unwrap();
238 /// // Get name of first challenge
239 /// println!("Total pages: {}", challenges.data.first().unwrap().name);
240 /// # }
241 /// ```
242 ///
243 /// ```no_run
244 /// # use codewars_api::rest_api::client::RestCodewarsClient;
245 /// # #[tokio::main]
246 /// # async fn main() {
247 /// # let client = RestCodewarsClient::new();
248 /// let challenges = client.get_authored_challenges("aaron.pp").await.unwrap();
249 /// // Check if list is not empty
250 /// assert!(!challenges.data.is_empty());
251 /// # }
252 /// ```
253 pub async fn get_authored_challenges(
254 &self,
255 username: &str,
256 ) -> Result<AuthoredChallenges, String> {
257 // Send request
258 let response = reqwest::get(format!(
259 "{}/api/v1/users/{}/code-challenges/authored",
260 self.host_name, username
261 ))
262 .await
263 .unwrap();
264 // Check status code
265 match response.status() {
266 reqwest::StatusCode::OK => match response.json::<AuthoredChallenges>().await {
267 // Return parsed response
268 Ok(parsed) => Ok(parsed),
269 // Return error if there is an error decoding the response body with serde
270 Err(err) => Err(err.to_string()),
271 },
272 // Return error if status code is not 200
273 other => Err(format!("unexpected status code: {}", other)),
274 }
275 }
276}
277
278/// Implement Default trait for RestCodewarsClient
279impl Default for RestCodewarsClient {
280 // Return default value of RestCodewarsClient
281 fn default() -> Self {
282 Self::new()
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 //! Tests for REST Client
289 //! All mocks are from Codewars documentation
290
291 use std::path::Path;
292 use super::{super::models::*, *};
293
294 /// Test getting a user
295 #[tokio::test]
296 async fn test_get_user() {
297 let mut server = mockito::Server::new_async().await;
298 let host = server.host_with_port();
299 let client = RestCodewarsClient::new_with_custom_host(format!("http://{}", host));
300 let content = std::fs::read_to_string(Path::new(&"tests/mocks/get_user.json")).unwrap();
301 let text: User = serde_json::from_str(&content).unwrap();
302 let mock = server.mock("GET", "/api/v1/users/some_user").with_status(200).with_header("content-type", "application/json").with_body(content).create_async().await;
303 let result = client.get_user("some_user").await.unwrap();
304 mock.assert_async().await;
305 assert_eq!(result, text);
306 }
307
308 /// Test getting completed challenges
309 #[tokio::test]
310 async fn test_get_completed_challenges() {
311 let mut server = mockito::Server::new_async().await;
312 let host = server.host_with_port();
313 let client = RestCodewarsClient::new_with_custom_host(format!("http://{}", host));
314 let content = std::fs::read_to_string(Path::new(&"tests/mocks/get_completed_challenges.json")).unwrap();
315 let text: CompletedChallenges = serde_json::from_str(&content).unwrap();
316 let mock = server.mock("GET", "/api/v1/users/some_user/code-challenges/completed?page=0").with_status(200).with_header("content-type", "application/json").with_body(content).create_async().await;
317 let result = client.get_completed_challenges("some_user", 0).await.unwrap();
318 mock.assert_async().await;
319 assert_eq!(result, text);
320 }
321
322 /// Test getting first page of completed challenges
323 #[tokio::test]
324 async fn test_get_completed_challenges_first_page() {
325 let mut server = mockito::Server::new_async().await;
326 let host = server.host_with_port();
327 let client = RestCodewarsClient::new_with_custom_host(format!("http://{}", host));
328 let content = std::fs::read_to_string(Path::new(&"tests/mocks/get_completed_challenges.json")).unwrap();
329 let text: CompletedChallenges = serde_json::from_str(&content).unwrap();
330 let mock = server.mock("GET", "/api/v1/users/some_user/code-challenges/completed?page=0").with_status(200).with_header("content-type", "application/json").with_body(content).create_async().await;
331 let result = client.get_completed_challenges_first_page("some_user").await.unwrap();
332 mock.assert_async().await;
333 assert_eq!(result, text);
334 }
335
336 /// Test getting authored challenges
337 #[tokio::test]
338 async fn test_get_authored_challenges() {
339 let mut server = mockito::Server::new_async().await;
340 let host = server.host_with_port();
341 let client = RestCodewarsClient::new_with_custom_host(format!("http://{}", host));
342 let content = std::fs::read_to_string(Path::new(&"tests/mocks/get_authored_challenges.json")).unwrap();
343 let text: AuthoredChallenges = serde_json::from_str(&content).unwrap();
344 let mock = server.mock("GET", "/api/v1/users/some_user/code-challenges/authored").with_status(200).with_header("content-type", "application/json").with_body(content).create_async().await;
345 let result = client.get_authored_challenges("some_user").await.unwrap();
346 mock.assert_async().await;
347 assert_eq!(result, text);
348 }
349
350 /// Test getting code challenge information
351 #[tokio::test]
352 async fn test_get_code_challenge() {
353 let mut server = mockito::Server::new_async().await;
354 let host = server.host_with_port();
355 let client = RestCodewarsClient::new_with_custom_host(format!("http://{}", host));
356 let content = std::fs::read_to_string(Path::new(&"tests/mocks/get_challenge.json")).unwrap();
357 let text: CodeChallenge = serde_json::from_str(&content).unwrap();
358 let mock = server.mock("GET", format!("/api/v1/code-challenges/{}", text.slug).as_str()).with_status(200).with_header("content-type", "application/json").with_body(content).create_async().await;
359 let result = client.get_kata(&text.slug).await.unwrap();
360 mock.assert_async().await;
361 assert_eq!(result, text);
362 }
363}