habitica_api/
client.rs

1use reqwest::{Client, ClientBuilder, Method, RequestBuilder, Response, Url};
2use reqwest::header::{HeaderMap, InvalidHeaderValue};
3use serde::{Serialize, de::DeserializeOwned};
4use core::fmt;
5
6#[cfg(test)]
7mod tests {
8    use super::*;
9
10    fn get_test_url() -> Url {
11        DEFAULT_ROOT.parse().expect("failed to parse default root url for testing")
12    }
13
14    fn get_test_auth() -> UserAuth {
15        UserAuth {
16            user_id: String::from("Some User"),
17            api_key: String::from("TEST TOKEN"),
18        }
19    }
20
21    fn get_test_client_id() -> ClientId {
22        ClientId {
23            creator_id: "Creator Id",
24            app_name: "habitica-api-rs-tests",
25        }
26    }
27
28    #[test]
29    fn test_client_default() {
30        let client = HabiticaClient::new(get_test_url(), get_test_client_id())
31            .expect("failed to build test client");
32        let root = client.root;
33        assert_eq!(client.auth, None);
34        assert_eq!(root.scheme(), "https");
35        assert_eq!(root.host_str(), Some("habitica.com"));
36        assert_eq!(root.path(), "/api/v3");
37        assert_eq!(root.query(), None);
38        assert_eq!(root.username(), "");
39        assert_eq!(root.password(), None);
40        assert_eq!(root.port(), None);
41        assert_eq!(root.fragment(), None);
42    }
43
44    #[test]
45    fn test_client_creation_does_not_modify_url() {
46        let url = get_test_url();
47        let new_client = HabiticaClient::new(url.clone(), get_test_client_id())
48            .expect("failed to build test client (without auth)");
49        let auth_client = HabiticaClient::with_auth(url.clone(), get_test_client_id(), get_test_auth())
50            .expect("failed to build test client (with auth)");
51        assert_eq!(new_client.root, url);
52        assert_eq!(auth_client.root, url);
53    }
54
55    #[test]
56    fn test_client_new_has_no_auth() {
57        let client = HabiticaClient::new(get_test_url(), get_test_client_id())
58            .expect("failed to build test client");
59        assert_eq!(client.auth, None);
60    }
61
62    #[test]
63    fn test_client_with_auth_has_unmodified_auth() {
64        let auth = get_test_auth();
65        let client = HabiticaClient::with_auth(get_test_url(), get_test_client_id(), auth.clone())
66            .expect("failed to build test client");
67        assert_eq!(client.auth, Some(auth));
68    }
69
70    #[test]
71    fn test_setting_auth() {
72        let mut client = HabiticaClient::new(get_test_url(), get_test_client_id())
73            .expect("failed to build test client");
74        let auth = get_test_auth();
75        assert_ne!(client.auth, Some(auth.clone()));
76        client.set_auth(auth.clone())
77            .expect("failed to manually set auth");
78        assert_eq!(client.auth, Some(auth))
79    }
80}
81
82pub const DEFAULT_ROOT: &str = "https://habitica.com/api/v3";
83pub const HEADER_KEY_XCLIENT: &str = "X-Client";
84pub const HEADER_KEY_API_USER: &str = "X-API-User";
85pub const HEADER_KEY_API_KEY: &str = "X-API-Key";
86
87pub type Result<T> = std::result::Result<T, Error>;
88
89#[derive(Debug)]
90pub enum Error {
91    BuildRequest(reqwest::Error),
92    BuildReqwestClient(reqwest::Error),
93    InvalidHeaderValue(InvalidHeaderValue),
94    UrlParse(url::ParseError),
95    Request(reqwest::Error),
96}
97
98impl From<url::ParseError> for Error {
99    fn from(v: url::ParseError) -> Self {
100        Error::UrlParse(v)
101    }
102}
103
104impl From<InvalidHeaderValue> for Error {
105    fn from(v: InvalidHeaderValue) -> Self {
106        Error::InvalidHeaderValue(v)
107    }
108}
109
110#[derive(Clone, Debug, PartialEq)]
111pub struct ClientId {
112    creator_id: &'static str,
113    app_name: &'static str,
114}
115
116impl fmt::Display for ClientId {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        write!(f, "{}-{}", self.creator_id, self.app_name)
119    }
120}
121
122#[derive(Clone, Debug, PartialEq)]
123pub struct UserAuth {
124    pub user_id: String,
125    pub api_key: String,
126}
127
128#[derive(Clone, Debug)]
129pub struct HabiticaClient {
130    root: Url,
131    client: Client,
132    client_id: ClientId,
133    auth: Option<UserAuth>,
134}
135
136impl HabiticaClient {
137    fn internal_new(root: Url, client_id: ClientId, auth: Option<UserAuth>) -> Result<Self> {
138        let client = Self::internal_new_client(&client_id, auth.as_ref())?
139            .build()
140            .map_err(Error::BuildReqwestClient)?;
141
142        let output = Self {
143            auth,
144            root,
145            client,
146            client_id,
147        };
148
149        Ok(output)
150    }
151
152    fn internal_new_client(client_id: &ClientId, auth: Option<&UserAuth>) -> Result<ClientBuilder> {
153        let mut headers = HeaderMap::new();
154        let client = Client::builder();
155
156        if let Some(auth) = auth {
157            headers.insert(HEADER_KEY_API_USER, auth.user_id.parse().map_err(Error::InvalidHeaderValue)?);
158            headers.insert(HEADER_KEY_API_KEY, auth.api_key.parse().map_err(Error::InvalidHeaderValue)?);
159        }
160
161        headers.insert(HEADER_KEY_XCLIENT, client_id.to_string().parse().map_err(Error::InvalidHeaderValue)?);
162
163        Ok(client.default_headers(headers))
164    }
165
166    pub fn new(root: Url, client_id: ClientId) -> Result<Self> {
167        Self::internal_new(root, client_id, None)
168    }
169
170    pub fn with_auth(root: Url, client_id: ClientId, auth: UserAuth) -> Result<Self> {
171        Self::internal_new(root, client_id, Some(auth))
172    }
173
174    pub fn set_auth(&mut self, auth: UserAuth) -> Result<()> {
175        self.auth = Some(auth);
176        self.client = Self::internal_new_client( &self.client_id, self.auth.as_ref())?
177            .build()
178            .map_err(Error::BuildReqwestClient)?;
179
180        Ok(())
181    }
182
183    pub async fn request<S>(&self, method: Method, path: &str, body: Option<S>, query: Option<S>) -> Result<Response>
184        where S: Serialize {
185        let new_url = self.root.join(path)?;
186        let mut req = self.client.request(method, new_url);
187        
188        if let Some(body) = body {
189            req = req.json(&body);
190        }
191        
192        if let Some(query) = query {
193            req = req.query(&query);
194        }
195
196        let req = req.build().map_err(Error::BuildRequest)?;
197
198        self.client.execute(req).await
199            .map_err(Error::Request)
200    }
201}