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