1use http::header::{Entry, HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
4use http::uri::Uri;
5use serde::ser::Serialize;
6
7use crate::error::{self, Error};
8use crate::request::{Filter, Request, TokenRequired};
9use crate::response::ResponseFuture;
10
11mod builder;
12mod conn;
13mod methods;
14
15pub(crate) mod service;
16
17pub use self::builder::Builder;
18
19pub const DEFAULT_HOST: &str = "api.mod.io";
20pub const TEST_HOST: &str = "api.test.mod.io";
21const API_VERSION: u8 = 1;
22
23const HDR_X_MODIO_PLATFORM: &str = "X-Modio-Platform";
24const HDR_X_MODIO_PORTAL: &str = "X-Modio-Portal";
25const HDR_FORM_URLENCODED: HeaderValue =
26 HeaderValue::from_static("application/x-www-form-urlencoded");
27
28pub struct Client {
30 http: service::Svc,
31 host: Box<str>,
32 api_key: Box<str>,
33 token: Option<Box<str>>,
34 headers: HeaderMap,
35}
36
37impl Client {
38 pub fn builder(api_key: String) -> Builder {
40 Builder::new(api_key)
41 }
42
43 pub fn api_key(&self) -> &str {
45 &self.api_key
46 }
47
48 pub fn token(&self) -> Option<&str> {
50 self.token.as_ref().and_then(|s| s.strip_prefix("Bearer "))
51 }
52
53 pub fn with_token(&self, token: String) -> Self {
55 Self {
56 http: self.http.clone(),
57 host: self.host.clone(),
58 api_key: self.api_key.clone(),
59 token: Some(builder::create_token(token)),
60 headers: self.headers.clone(),
61 }
62 }
63
64 pub(crate) fn raw_request(&self, req: Request) -> service::ResponseFuture {
65 self.http.request(req)
66 }
67
68 pub(crate) fn request<T>(&self, req: Request) -> ResponseFuture<T> {
69 match self.try_request(req) {
70 Ok(fut) => fut,
71 Err(err) => ResponseFuture::failed(err),
72 }
73 }
74
75 fn try_request<T>(&self, req: Request) -> Result<ResponseFuture<T>, Error> {
76 let (mut parts, body) = req.into_parts();
77
78 let mut uri = UriBuilder::new(&self.host, &parts.uri);
79
80 let token_required = parts.extensions.get();
81 match (token_required, &self.token) {
82 (Some(TokenRequired(false)) | None, _) => {
83 uri.api_key(&self.api_key);
84 }
85 (Some(TokenRequired(true)), Some(token)) => match HeaderValue::from_str(token) {
86 Ok(mut value) => {
87 value.set_sensitive(true);
88 parts.headers.insert(AUTHORIZATION, value);
89 }
90 Err(e) => return Err(error::request(e)),
91 },
92 (Some(TokenRequired(true)), None) => return Err(error::token_required()),
93 }
94
95 if let Some(filter) = parts.extensions.get::<Filter>() {
96 uri.filter(filter)?;
97 }
98
99 parts.uri = uri.build()?;
100
101 for (key, value) in &self.headers {
102 if let Entry::Vacant(entry) = parts.headers.entry(key) {
103 entry.insert(value.clone());
104 }
105 }
106
107 if let Entry::Vacant(entry) = parts.headers.entry(CONTENT_TYPE) {
108 entry.insert(HDR_FORM_URLENCODED);
109 }
110
111 let fut = self.http.request(Request::from_parts(parts, body));
112
113 Ok(ResponseFuture::new(fut))
114 }
115}
116
117struct UriBuilder<'a> {
118 serializer: form_urlencoded::Serializer<'a, String>,
119}
120
121impl<'a> UriBuilder<'a> {
122 fn new(host: &'a str, path: &'a Uri) -> UriBuilder<'a> {
123 let mut uri = format!("https://{host}/v{API_VERSION}{path}");
124
125 let query_start = if let Some(start) = uri.find('?') {
126 start
127 } else {
128 uri.push('?');
129 uri.len()
130 };
131
132 Self {
133 serializer: form_urlencoded::Serializer::for_suffix(uri, query_start),
134 }
135 }
136
137 fn api_key(&mut self, value: &str) {
138 self.serializer.append_pair("api_key", value);
139 }
140
141 fn filter(&mut self, filter: &Filter) -> Result<(), Error> {
142 filter
143 .serialize(serde_urlencoded::Serializer::new(&mut self.serializer))
144 .map_err(error::request)?;
145
146 Ok(())
147 }
148
149 fn build(mut self) -> Result<Uri, Error> {
150 self.serializer
151 .finish()
152 .trim_end_matches('?')
153 .parse()
154 .map_err(error::request)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn basic_uri() {
164 let path = Uri::from_static("/games/1/mods/2");
165 let uri = UriBuilder::new(DEFAULT_HOST, &path);
166
167 let uri = uri.build().unwrap();
168 assert_eq!("https://api.mod.io/v1/games/1/mods/2", uri);
169 }
170
171 #[test]
172 fn uri_with_api_key() {
173 let path = Uri::from_static("/games/1/mods/2");
174 let mut uri = UriBuilder::new(DEFAULT_HOST, &path);
175
176 uri.api_key("FOOBAR");
177
178 let uri = uri.build().unwrap();
179 assert_eq!("https://api.mod.io/v1/games/1/mods/2?api_key=FOOBAR", uri);
180 }
181
182 #[test]
183 fn uri_with_filter() {
184 let path = Uri::from_static("/games/1/mods/2");
185 let mut uri = UriBuilder::new(DEFAULT_HOST, &path);
186
187 uri.filter(&Filter::with_limit(123)).unwrap();
188
189 let uri = uri.build().unwrap();
190 assert_eq!("https://api.mod.io/v1/games/1/mods/2?_limit=123", uri);
191 }
192
193 #[test]
194 fn uri_with_path_and_query() {
195 let path = Uri::from_static("/games/1/mods/2?foo=bar");
196 let mut uri = UriBuilder::new(DEFAULT_HOST, &path);
197
198 uri.filter(&Filter::with_limit(123)).unwrap();
199
200 let uri = uri.build().unwrap();
201 assert_eq!(
202 "https://api.mod.io/v1/games/1/mods/2?foo=bar&_limit=123",
203 uri
204 );
205 }
206}