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}