modio/client/
mod.rs

1//! HTTP client for the mod.io API.
2
3use 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
28/// HTTP client for the mod.io API.
29pub 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    /// Create a new builder with an API key.
39    pub fn builder(api_key: String) -> Builder {
40        Builder::new(api_key)
41    }
42
43    /// Retrieve the API key used by the client.
44    pub fn api_key(&self) -> &str {
45        &self.api_key
46    }
47
48    /// Retrieve the token used by the client.
49    pub fn token(&self) -> Option<&str> {
50        self.token.as_ref().and_then(|s| s.strip_prefix("Bearer "))
51    }
52
53    /// Create a new client from the current instance with the given token.
54    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}