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}