Skip to main content

yesser_todo_api/
lib.rs

1pub mod helpers;
2
3use yesser_todo_db::Task;
4
5pub use yesser_todo_errors::api_error::ApiError;
6use yesser_todo_errors::server_error::ServerError;
7
8use crate::helpers::is_success;
9
10pub const DEFAULT_PORT: &str = "6982";
11
12pub struct Client {
13    pub hostname: String,
14    pub port: String,
15    client: ureq::Agent,
16}
17
18impl Client {
19    /// Creates a new `Client` for the given hostname.
20    ///
21    /// If `port` is `None`, the client will use the default port `"6982"`.
22    ///
23    /// # Examples
24    ///
25    /// ```
26    /// # use yesser_todo_api::{Client, DEFAULT_PORT};
27    ///
28    /// let c = Client::new("http://127.0.0.1".to_string(), None);
29    /// # assert_eq!(c.hostname, "http://127.0.0.1");
30    /// # assert_eq!(c.port, DEFAULT_PORT);
31    /// ```
32    pub fn new(hostname: String, port: Option<String>) -> Client {
33        Client {
34            hostname,
35            port: port.unwrap_or_else(|| DEFAULT_PORT.to_string()),
36            client: ureq::Agent::config_builder().http_status_as_error(false).build().into(),
37        }
38    }
39
40    /// Retrieves all tasks from the configured server.
41    ///
42    /// # Returns
43    ///
44    /// A tuple `(u16, Vec<Task>)` where `u16` is the HTTP response status and `Vec<Task>` is the list of tasks.
45    ///
46    /// # Examples
47    ///
48    /// ```no_run
49    /// use yesser_todo_api::Client;
50    /// use yesser_todo_db::Task;
51    ///
52    /// # fn example() -> Option<Vec<Task>> {
53    /// let client = Client::new("http://127.0.0.1".into(), None);
54    /// let (status, tasks) = client.get().ok()?;
55    /// // `tasks` is a Vec<yesser_todo_db::Task>
56    /// # Some(tasks)
57    /// # }
58    /// ```
59    pub fn get(&self) -> Result<(u16, Vec<Task>), ApiError> {
60        let mut result = self.client.get(format!("{}:{}/tasks", self.hostname, self.port).as_str()).call()?;
61
62        let status_code = result.status();
63        if status_code.is_success() {
64            let result = result.body_mut().read_json::<Vec<Task>>();
65            match result {
66                Ok(result) => Ok((status_code.as_u16(), result)),
67                Err(err) => Err(ApiError::RequestError(err)),
68            }
69        } else {
70            match result.body_mut().read_json::<ServerError>() {
71                Ok(err) => Err(ApiError::ServerError(err)),
72                Err(_) => Err(ApiError::HTTPError(status_code.as_u16())),
73            }
74        }
75    }
76
77    /// Adds a new task with the given name to the to-do service.
78    ///
79    /// Sends the task name as JSON to the service's `/add` endpoint and returns the HTTP status
80    /// together with the created `Task` parsed from the response.
81    ///
82    /// # Examples
83    ///
84    /// ```no_run
85    /// use yesser_todo_api::{ Client, helpers::is_success };
86    ///
87    /// fn example_add() {
88    ///     let client = Client::new("http://127.0.0.1".to_string(), None);
89    ///     let (status, task) = client.add("example task").unwrap();
90    ///     assert!(is_success(status));
91    ///     assert_eq!(task.name, "example task");
92    /// }
93    /// ```
94    pub fn add(&self, task_name: &str) -> Result<(u16, Task), ApiError> {
95        let mut result = self.client.post(format!("{}:{}/add", self.hostname, self.port).as_str()).send_json(task_name)?;
96
97        let status_code = result.status();
98        if status_code.is_success() {
99            let result = result.body_mut().read_json::<Task>();
100            match result {
101                Ok(result) => Ok((status_code.as_u16(), result)),
102                Err(err) => Err(ApiError::RequestError(err)),
103            }
104        } else {
105            match result.body_mut().read_json::<ServerError>() {
106                Ok(err) => Err(ApiError::ServerError(err)),
107                Err(_) => Err(ApiError::HTTPError(status_code.as_u16())),
108            }
109        }
110    }
111
112    /// Retrieves the zero-based index of the task with the given name from the API.
113    ///
114    /// Returns the HTTP response status together with the parsed index on success.
115    ///
116    /// # Examples
117    ///
118    /// ```no_run
119    /// use yesser_todo_api::Client;
120    ///
121    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
122    /// let client = Client::new("http://127.0.0.1".into(), None);
123    /// let (status, index) = client.get_index("example-task")?;
124    /// println!("status: {}, index: {}", status, index);
125    /// # Ok(()) }
126    /// ```
127    pub fn get_index(&self, task_name: &str) -> Result<(u16, usize), ApiError> {
128        let mut result = self
129            .client
130            .get(format!("{}:{}/index", self.hostname, self.port).as_str())
131            .query("name", task_name)
132            .call()?;
133        let status_code = result.status();
134        if status_code.is_success() {
135            let result = result.body_mut().read_json::<usize>();
136            match result {
137                Ok(result) => Ok((status_code.as_u16(), result)),
138                Err(err) => Err(ApiError::RequestError(err)),
139            }
140        } else {
141            match result.body_mut().read_json::<ServerError>() {
142                Ok(err) => Err(ApiError::ServerError(err)),
143                Err(_) => Err(ApiError::HTTPError(status_code.as_u16())),
144            }
145        }
146    }
147
148    /// Delete the task with the given name from the remote server.
149    ///
150    /// Resolves the task's index on the server and sends a DELETE request for that index.
151    ///
152    /// # Returns
153    ///
154    /// `Ok(u16)` if the server responded with a success status for the delete request, `Err(ApiError)` on transport or HTTP error.
155    ///
156    /// # Examples
157    ///
158    /// ```no_run
159    /// # use yesser_todo_api::Client;
160    /// fn main() {
161    ///     let client = Client::new("http://127.0.0.1".to_string(), None);
162    ///     let _status = client.remove("example-task");
163    /// }
164    /// ```
165    pub fn remove(&self, task_name: &str) -> Result<u16, ApiError> {
166        let index_result = self.get_index(task_name);
167        let index = match index_result {
168            Ok((_, result)) => result,
169            Err(err) => return Err(err),
170        };
171
172        let mut result = self
173            .client
174            .delete(format!("{}:{}/remove", self.hostname, self.port).as_str())
175            .query("index", index.to_string())
176            .call()?;
177        if result.status().is_success() {
178            Ok(result.status().as_u16())
179        } else {
180            let status_code = result.status();
181            match result.body_mut().read_json::<ServerError>() {
182                Ok(err) => Err(ApiError::ServerError(err)),
183                Err(_) => Err(ApiError::HTTPError(status_code.as_u16())),
184            }
185        }
186    }
187
188    /// Mark the task with the given name as done and return the server status and the updated task.
189    ///
190    /// On success returns a tuple containing the response `u16` status code and the `Task` as returned by the server.
191    /// On failure returns an `Err(ApiError)`.
192    ///
193    /// # Examples
194    ///
195    /// ```no_run
196    /// # use yesser_todo_api::Client;
197    /// # use yesser_todo_db::Task;
198    /// # fn _example() {
199    /// let client = Client::new("http://127.0.0.1".to_string(), None);
200    /// let res = client.done("test");
201    /// match res {
202    ///     Ok((status, task)) => {
203    ///         let _ = task.name;
204    ///     }
205    ///     Err(e) => panic!("request failed: {:?}", e),
206    /// }
207    /// # }
208    /// ```
209    pub fn done(&self, task_name: &str) -> Result<(u16, Task), ApiError> {
210        let index = match self.get_index(task_name) {
211            Ok((status_code, result)) => {
212                if !is_success(status_code) {
213                    return Err(ApiError::HTTPError(status_code));
214                }
215                result
216            }
217            Err(err) => return Err(err),
218        };
219        let mut result = self.client.post(format!("{}:{}/done", self.hostname, self.port).as_str()).send_json(index)?;
220        let status_code = result.status();
221        if status_code.is_success() {
222            let result = result.body_mut().read_json::<Task>();
223            match result {
224                Ok(result) => Ok((status_code.as_u16(), result)),
225                Err(err) => Err(ApiError::RequestError(err)),
226            }
227        } else {
228            let status_code = result.status();
229            match result.body_mut().read_json::<ServerError>() {
230                Ok(err) => Err(ApiError::ServerError(err)),
231                Err(_) => Err(ApiError::HTTPError(status_code.as_u16())),
232            }
233        }
234    }
235
236    /// Mark the task identified by `task_name` as not done and return the updated task.
237    ///
238    /// # Returns
239    ///
240    /// `Ok((u16, Task))` with the HTTP response status and the updated task on success, `Err(ApiError)` on failure.
241    ///
242    /// # Examples
243    ///
244    /// ```no_run
245    /// use yesser_todo_api::Client;
246    ///
247    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
248    /// let client = Client::new("http://127.0.0.1".to_string(), None);
249    /// let res = client.undone("example")?;
250    /// # Ok(())
251    /// # }
252    /// ```
253    pub fn undone(&self, task_name: &str) -> Result<(u16, Task), ApiError> {
254        let index_result = self.get_index(task_name);
255        let index = match index_result {
256            Ok((status_code, result)) => {
257                if !is_success(status_code) {
258                    return Err(ApiError::HTTPError(status_code));
259                }
260                result
261            }
262            Err(err) => return Err(err),
263        };
264
265        let mut result = self.client.post(format!("{}:{}/undone", self.hostname, self.port).as_str()).send_json(index)?;
266        let status_code = result.status();
267        if status_code.is_success() {
268            let result = result.body_mut().read_json::<Task>();
269            match result {
270                Ok(result) => Ok((status_code.as_u16(), result)),
271                Err(err) => Err(ApiError::RequestError(err)),
272            }
273        } else {
274            let status_code = result.status();
275            match result.body_mut().read_json::<ServerError>() {
276                Ok(err) => Err(ApiError::ServerError(err)),
277                Err(_) => Err(ApiError::HTTPError(status_code.as_u16())),
278            }
279        }
280    }
281
282    /// Clears all tasks on the remote to-do service.
283    ///
284    /// Sends a DELETE request to the configured `/clear` endpoint and returns the HTTP status code.
285    ///
286    /// # Returns
287    /// - status code of the request;
288    /// - on failure returns `Err(ApiError)`
289    ///
290    /// # Examples
291    ///
292    /// ```no_run
293    /// # use yesser_todo_api::Client;
294    /// # fn example() {
295    /// let client = Client::new("http://127.0.0.1".to_string(), None);
296    /// let status = client.clear().unwrap();
297    /// assert_eq!(status, 200);
298    /// # }
299    /// ```
300    pub fn clear(&self) -> Result<u16, ApiError> {
301        let result = self.client.delete(format!("{}:{}/clear", self.hostname, self.port).as_str()).call()?;
302        if result.status().is_success() {
303            Ok(result.status().as_u16())
304        } else {
305            Err(ApiError::HTTPError(result.status().as_u16()))
306        }
307    }
308
309    /// Clears all tasks marked as done on the remote to-do service.
310    ///
311    /// Sends a DELETE request to "{hostname}:{port}/cleardone".
312    ///
313    /// # Returns
314    ///
315    /// `Ok(u16)` with the HTTP response status when the request succeeds, `Err(ApiError)` otherwise.
316    ///
317    /// # Examples
318    ///
319    /// ```no_run
320    /// use yesser_todo_api::Client;
321    /// use yesser_todo_api::helpers::is_success;
322    ///
323    /// let client = Client::new("http://127.0.0.1".into(), None);
324    /// let status = client.clear_done().unwrap();
325    /// # assert!(is_success(status));
326    /// ```
327    pub fn clear_done(&self) -> Result<u16, ApiError> {
328        let result = self.client.delete(format!("{}:{}/cleardone", self.hostname, self.port).as_str()).call()?;
329        if result.status().is_success() {
330            Ok(result.status().as_u16())
331        } else {
332            Err(ApiError::HTTPError(result.status().as_u16()))
333        }
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn get() {
343        let client = Client::new("http://127.0.0.1".to_string(), None);
344        let result = client.get();
345        println!("{:?}", result);
346        assert!(result.is_ok() && is_success(result.unwrap().0));
347    }
348
349    #[test]
350    fn add_get_index_done_undone_remove() {
351        let client = Client::new("http://127.0.0.1".to_string(), None);
352        // add
353        let result = client.add("test");
354        println!("{:?}", result);
355        assert!(result.is_ok() && is_success(result.unwrap().0));
356        // get_index
357        let result = client.get_index("test");
358        println!("{:?}", result);
359        assert!(result.is_ok() && is_success(result.unwrap().0));
360        // done
361        let result = client.done("test");
362        println!("{:?}", result);
363        assert!(result.is_ok() && is_success(result.unwrap().0));
364        // undone
365        let result = client.undone("test");
366        println!("{:?}", result);
367        assert!(result.is_ok() && is_success(result.unwrap().0));
368        // remove
369        let result = client.remove("test");
370        println!("{:?}", result);
371        assert!(result.is_ok() && is_success(result.unwrap()));
372
373        // cleanup
374        let result = client.clear();
375        println!("{:?}", result);
376        assert!(result.is_ok() && is_success(result.unwrap()));
377    }
378
379    #[test]
380    fn clear() {
381        let client = Client::new("http://127.0.0.1".to_string(), None);
382        let _ = client.add("test");
383        let _ = client.add("test");
384        let _ = client.add("test");
385        let result = client.clear();
386        println!("{:?}", result);
387        assert!(result.is_ok());
388        let result = client.get();
389        println!("{:?}", result);
390        assert!(result.is_ok());
391        let unwrapped = result.unwrap();
392        assert!(is_success(unwrapped.0) && unwrapped.1.is_empty());
393    }
394
395    #[test]
396    fn clear_done() {
397        let client = Client::new("http://127.0.0.1".to_string(), None);
398        let _ = client.add("test1");
399        let _ = client.add("test2");
400        let _ = client.add("test3");
401        let _ = client.done("test1");
402        let _ = client.done("test3");
403        let result = client.clear_done();
404        println!("{:?}", result);
405        assert!(result.is_ok());
406        let result = client.get();
407        println!("{:?}", result);
408        assert!(result.is_ok());
409        let unwrapped = result.unwrap();
410        assert!(is_success(unwrapped.0) && unwrapped.1.len() == 1 && unwrapped.1[0].name == "test2");
411
412        // cleanup
413        let result = client.clear();
414        println!("{:?}", result);
415        assert!(result.is_ok() && is_success(result.unwrap()));
416    }
417}