modio/client/
mod.rs

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