rust_freely/client/
client.rs

1/// This module contains the main [Client] struct, which provides access to all of the other types & methods.
2pub mod api_client {
3    use serde_derive::{Deserialize, Serialize};
4
5    use crate::{api_handlers::{CollectionHandler, PostHandler, UserHandler}, api_models, api_wrapper::Api};
6
7    #[derive(Clone, Serialize, Deserialize, Debug)]
8    /// The desired authentication method
9    pub enum Auth {
10        /// Authenticate with an API token
11        Token(String),
12
13        /// Authenticate with a username and password
14        Login{
15            /// Login username
16            username: String, 
17
18            /// Login password
19            password: String
20        }
21    }
22
23    #[derive(Clone, Serialize, Deserialize, Debug)]
24    /// Represents a request error (see [ApiError])
25    pub struct RequestError {
26        /// Error code (HTTP status)
27        pub code: u16,
28
29        /// Optional result information
30        pub reason: Option<String>
31    }
32
33    #[derive(Clone, Serialize, Deserialize, Debug)]
34    #[serde(tag = "type")]
35    /// The main Error enum for this library
36    pub enum ApiError {
37        /// Raised if the API returns a non-success status code
38        Request{
39            /// RequestError instance
40            error: RequestError
41        },
42
43        /// Raised if authentication fails
44        AuthenticationError{},
45
46        /// Raised on an unexpected error. Should never appear in normal operation
47        UnknownError{},
48
49        /// Raised if URL creation fails
50        UrlError{},
51
52        /// Raised if data parsing fails
53        ParseError{
54            /// Text that serde failed to parse
55            text: String
56        },
57
58        /// Raised if connecting to the API server fails
59        ConnectionError{},
60
61        /// Raised if an action cannot be performed when logged out
62        LoggedOut{},
63
64        /// Raised if invalid data was passed from the user, or if no [Client] instance is defined on the referenced struct
65        UsageError{}
66    }
67
68
69    #[derive(Clone, Serialize, Deserialize, Debug)]
70    /// Main Client struct
71    pub struct Client {
72        _base_url: String,
73        _token: Option<String>,
74    }
75
76    impl Client {
77        /// Creates a new client with a base URL
78        pub fn new(base: String) -> Self {
79            Client { _base_url: base, _token: None }
80        }
81
82        /// Authenticates with an [Auth] enum value
83        pub async fn authenticate(&mut self, auth: Auth) -> Result<Self, ApiError> {
84            match auth {
85                Auth::Token(token) => {
86                    self._token = Some(token);
87                    Ok(self.clone())
88                },
89                Auth::Login { username, password } => {
90                    match self.api().post::<api_models::responses::Login, _>("/auth/login", Some(api_models::requests::Login {alias: username, pass: password})).await {
91                        Ok(data) => {
92                            self._token = Some(data.access_token);
93                            Ok(self.clone())
94                        },
95                        Err(e) => Err(e)
96                    }
97                }
98            }
99        }
100
101        /// Deauthenticates from the server
102        pub async fn logout(&mut self) -> Result<Self, ApiError> {
103            if self.is_authenticated() {
104                match self.api().delete("/auth/me").await {
105                    Ok(_) => {
106                        self._token = None;
107                        Ok(self.clone())
108                    },
109                    Err(e) => Err(e)
110                }
111            } else {
112                Err(ApiError::LoggedOut {  })
113            }
114        }
115
116        /// Retrieves the base URL
117        pub fn url(&self) -> String {
118            self._base_url.clone()
119        }
120
121        /// Retrieves the access token
122        pub fn token(&self) -> Option<String> {
123            self._token.clone()
124        }
125
126        /// Checks if the instance is authenticated
127        pub fn is_authenticated(&self) -> bool {
128            self._token.is_some()
129        }
130
131        /// Returns a new [Api] instance. In general, a new instance should be created for each separate operation to prevent cloned [Client] desync.
132        pub fn api(&self) -> Api {
133            Api::new(self.clone())
134        }
135
136        /// Returns a wrapper around User methods
137        pub async fn user(&self) -> Result<UserHandler, ApiError> {
138            if self.is_authenticated() {
139                Ok(UserHandler::new(self.clone()).await)
140            } else {
141                Err(ApiError::LoggedOut {  })
142            }
143        }
144
145        /// Returns a wrapper around Post methods
146        pub fn posts(&self) -> PostHandler {
147            PostHandler::new(self.clone())
148        }
149
150        /// Returns a wrapper around Collection methods
151        pub fn collections(&self) -> CollectionHandler {
152            CollectionHandler::new(self.clone())
153        }
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use std::{thread::sleep, time::Duration};
160
161    use super::*;
162    use api_client::{Auth, Client};
163    use tokio_test;
164
165    macro_rules! aw {
166        ($e:expr) => {
167            tokio_test::block_on($e)
168        };
169    }
170
171    fn anon() -> Client {
172        Client::new("http://0.0.0.0:8080".to_string())
173    }
174
175    async fn auth() -> Client {
176        Client::new("http://0.0.0.0:8080".to_string()).authenticate(Auth::Login { username: "username".to_string(), password: "password".to_string() }).await.unwrap()
177    }
178
179    #[test]
180    fn eq_url() {
181        assert_eq!(anon().url(), "http://0.0.0.0:8080".to_string());
182    }
183
184    #[test]
185    fn anon_no_token() {
186        assert!(!anon().is_authenticated());
187    }
188
189    #[test]
190    fn auth_has_token() {
191        assert!(aw!(auth()).is_authenticated())
192    }
193
194    #[test]
195    fn auth_bad_login() {
196        assert!(aw!(anon().authenticate(Auth::Login { username: "usernameee".to_string(), password: "passwordeee".to_string() })).is_err())
197    }
198
199    #[test]
200    fn auth_logout() {
201        let mut authed = aw!(auth());
202        println!("{:?}", authed.clone().token());
203        sleep(Duration::from_secs(2));
204        let logged_out = aw!(authed.logout());
205        
206        assert!(!logged_out.unwrap().is_authenticated());
207    }
208
209}