1use std::sync::Arc;
2
3use http::header::USER_AGENT;
4use http::header::{HeaderMap, HeaderValue};
5use reqwest::{Client, ClientBuilder, Proxy};
6
7use crate::auth::Credentials;
8use crate::error::{self, Error, Result};
9use crate::types::id::{GameId, UserId};
10use crate::{TargetPlatform, TargetPortal};
11
12use super::{ClientRef, Modio};
13use super::{DEFAULT_AGENT, DEFAULT_HOST, TEST_HOST};
14
15#[must_use]
17pub struct Builder {
18 config: Config,
19}
20
21struct Config {
22 host: Option<String>,
23 credentials: Credentials,
24 builder: Option<ClientBuilder>,
25 headers: HeaderMap,
26 proxies: Vec<Proxy>,
27 #[cfg(feature = "__tls")]
28 tls: TlsBackend,
29 error: Option<Error>,
30}
31
32#[cfg(feature = "__tls")]
33enum TlsBackend {
34 #[cfg(feature = "default-tls")]
35 Default,
36 #[cfg(feature = "rustls-tls")]
37 Rustls,
38}
39
40#[cfg(feature = "__tls")]
41#[allow(clippy::derivable_impls)]
42impl Default for TlsBackend {
43 fn default() -> TlsBackend {
44 #[cfg(feature = "default-tls")]
45 {
46 TlsBackend::Default
47 }
48 #[cfg(all(feature = "rustls-tls", not(feature = "default-tls")))]
49 {
50 TlsBackend::Rustls
51 }
52 }
53}
54
55impl Builder {
56 pub fn new<C: Into<Credentials>>(credentials: C) -> Builder {
60 Builder {
61 config: Config {
62 host: None,
63 credentials: credentials.into(),
64 builder: None,
65 headers: HeaderMap::new(),
66 proxies: Vec::new(),
67 #[cfg(feature = "__tls")]
68 tls: TlsBackend::default(),
69 error: None,
70 },
71 }
72 }
73
74 pub fn build(self) -> Result<Modio> {
76 let config = self.config;
77
78 if let Some(e) = config.error {
79 return Err(e);
80 }
81
82 let host = config.host.unwrap_or_else(|| DEFAULT_HOST.to_string());
83 let credentials = config.credentials;
84
85 let client = {
86 let mut builder = {
87 let builder = config.builder.unwrap_or_else(Client::builder);
88 #[cfg(feature = "__tls")]
89 match config.tls {
90 #[cfg(feature = "default-tls")]
91 TlsBackend::Default => builder.use_native_tls(),
92 #[cfg(feature = "rustls-tls")]
93 TlsBackend::Rustls => builder.use_rustls_tls(),
94 }
95
96 #[cfg(not(feature = "__tls"))]
97 builder
98 };
99
100 let mut headers = config.headers;
101 if !headers.contains_key(USER_AGENT) {
102 headers.insert(USER_AGENT, HeaderValue::from_static(DEFAULT_AGENT));
103 }
104
105 for proxy in config.proxies {
106 builder = builder.proxy(proxy);
107 }
108
109 builder
110 .default_headers(headers)
111 .build()
112 .map_err(error::builder)?
113 };
114
115 Ok(Modio {
116 inner: Arc::new(ClientRef {
117 host,
118 client,
119 credentials,
120 }),
121 })
122 }
123
124 pub fn client<F>(mut self, f: F) -> Builder
126 where
127 F: FnOnce(ClientBuilder) -> ClientBuilder,
128 {
129 self.config.builder = Some(f(Client::builder()));
130 self
131 }
132
133 pub fn game_host(mut self, game_id: GameId) -> Self {
135 self.config.host = Some(format!("https://g-{game_id}.modapi.io/v1"));
136 self
137 }
138
139 pub fn user_host(mut self, user_id: UserId) -> Self {
141 self.config.host = Some(format!("https://u-{user_id}.modapi.io/v1"));
142 self
143 }
144
145 pub fn host<S: Into<String>>(mut self, host: S) -> Builder {
149 self.config.host = Some(host.into());
150 self
151 }
152
153 pub fn use_test(mut self) -> Builder {
155 self.config.host = Some(TEST_HOST.into());
156 self
157 }
158
159 pub fn user_agent<V>(mut self, value: V) -> Builder
163 where
164 V: TryInto<HeaderValue>,
165 V::Error: Into<http::Error>,
166 {
167 match value.try_into() {
168 Ok(value) => {
169 self.config.headers.insert(USER_AGENT, value);
170 }
171 Err(e) => {
172 self.config.error = Some(error::builder(e.into()));
173 }
174 }
175 self
176 }
177
178 pub fn proxy(mut self, proxy: Proxy) -> Builder {
180 self.config.proxies.push(proxy);
181 self
182 }
183
184 pub fn target_platform(mut self, platform: TargetPlatform) -> Builder {
188 match HeaderValue::from_str(platform.as_str()) {
189 Ok(value) => {
190 self.config.headers.insert("X-Modio-Platform", value);
191 }
192 Err(e) => {
193 self.config.error = Some(error::builder(e));
194 }
195 }
196 self
197 }
198
199 pub fn target_portal(mut self, portal: TargetPortal) -> Builder {
203 match HeaderValue::from_str(portal.as_str()) {
204 Ok(value) => {
205 self.config.headers.insert("X-Modio-Portal", value);
206 }
207 Err(e) => {
208 self.config.error = Some(error::builder(e));
209 }
210 }
211 self
212 }
213
214 #[cfg(feature = "default-tls")]
216 pub fn use_default_tls(mut self) -> Builder {
217 self.config.tls = TlsBackend::Default;
218 self
219 }
220
221 #[cfg(feature = "rustls-tls")]
223 pub fn use_rustls_tls(mut self) -> Builder {
224 self.config.tls = TlsBackend::Rustls;
225 self
226 }
227}