1use crate::{prelude::*, spec::Spec};
4use reqwest::{header, Client};
5use std::time::Duration;
6
7#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
67pub struct EsiBuilder {
68 pub(crate) compatibility_date: Option<String>,
69 pub(crate) client_id: Option<String>,
70 pub(crate) client_secret: Option<String>,
71 pub(crate) application_auth: Option<bool>,
72 pub(crate) callback_url: Option<String>,
73 pub(crate) base_api_url: Option<String>,
74 pub(crate) authorize_url: Option<String>,
75 pub(crate) token_url: Option<String>,
76 pub(crate) spec_url: Option<String>,
77 pub(crate) scope: Option<String>,
78 pub(crate) access_token: Option<String>,
79 pub(crate) access_expiration: Option<i64>,
80 pub(crate) refresh_token: Option<String>,
81 pub(crate) user_agent: Option<String>,
82 pub(crate) http_timeout: Option<u64>,
83 pub(crate) spec: Option<Spec>,
84}
85
86impl EsiBuilder {
87 pub fn new() -> Self {
89 Default::default()
90 }
91
92 pub fn compatibility_date(mut self, val: &str) -> Self {
96 self.compatibility_date = Some(val.to_owned());
97 self
98 }
99
100 pub fn client_id(mut self, val: &str) -> Self {
102 self.client_id = Some(val.to_owned());
103 self
104 }
105
106 pub fn client_secret(mut self, val: &str) -> Self {
108 self.client_secret = Some(val.to_owned());
109 self
110 }
111
112 pub fn enable_application_authentication(mut self, val: bool) -> Self {
114 self.application_auth = Some(val);
115 self
116 }
117
118 pub fn callback_url(mut self, val: &str) -> Self {
120 self.callback_url = Some(val.to_owned());
121 self
122 }
123
124 pub fn base_api_url(mut self, val: &str) -> Self {
126 self.base_api_url = Some(val.to_owned());
127 self
128 }
129
130 pub fn authorize_url(mut self, val: &str) -> Self {
132 self.authorize_url = Some(val.to_owned());
133 self
134 }
135
136 pub fn token_url(mut self, val: &str) -> Self {
138 self.token_url = Some(val.to_owned());
139 self
140 }
141
142 pub fn spec_url(mut self, val: &str) -> Self {
144 self.spec_url = Some(val.to_owned());
145 self
146 }
147
148 pub fn scope(mut self, val: &str) -> Self {
150 self.scope = Some(val.to_owned().replace(' ', "%20"));
151 self
152 }
153
154 pub fn access_token(mut self, val: Option<&str>) -> Self {
156 self.access_token = val.map(|v| v.to_owned());
157 self
158 }
159
160 pub fn access_expiration(mut self, val: Option<i64>) -> Self {
162 self.access_expiration = val;
163 self
164 }
165
166 pub fn refresh_token(mut self, val: Option<&str>) -> Self {
168 self.refresh_token = val.map(|v| v.to_owned());
169 self
170 }
171
172 pub fn user_agent(mut self, val: &str) -> Self {
174 self.user_agent = Some(val.to_owned());
175 self
176 }
177
178 pub fn http_timeout(mut self, val: Option<u64>) -> Self {
182 self.http_timeout = val;
183 self
184 }
185
186 pub fn spec(mut self, spec: Option<Spec>) -> Self {
195 self.spec = spec;
196 self
197 }
198
199 pub(crate) fn construct_client(&self) -> EsiResult<Client> {
200 let http_timeout = self
201 .http_timeout
202 .map(Duration::from_millis)
203 .unwrap_or_else(|| Duration::from_secs(60));
204 let headers = {
205 let mut map = header::HeaderMap::new();
206 let user_agent = &self
207 .user_agent
208 .as_ref()
209 .ok_or_else(|| EsiError::EmptyClientValue("user_agent".to_owned()))?
210 .to_owned();
211 map.insert(
212 header::USER_AGENT,
213 header::HeaderValue::from_str(user_agent)?,
214 );
215 map.insert(
216 header::ACCEPT,
217 header::HeaderValue::from_static("application/json"),
218 );
219 map
220 };
221 let builder = Client::builder()
222 .timeout(http_timeout)
223 .default_headers(headers);
224 #[cfg(feature = "rustls-tls")]
225 let builder = builder.tls_backend_rustls();
226 Ok(builder.build()?)
227 }
228
229 pub fn build(self) -> EsiResult<Esi> {
235 Esi::from_builder(self)
236 }
237}
238
239#[cfg(test)]
240mod tests {
241 use super::EsiBuilder;
242 use crate::spec::Spec;
243
244 #[test]
245 fn test_builder_valid() {
246 let b = EsiBuilder::new()
247 .client_id("a")
248 .client_secret("b")
249 .callback_url("c")
250 .user_agent("d")
251 .build()
252 .unwrap();
253
254 assert_eq!(b.client_id, Some(String::from("a")));
255 assert_eq!(b.client_secret, Some(String::from("b")));
256 assert_eq!(b.callback_url, Some(String::from("c")));
257 assert_eq!(b.compatibility_date, "2026-08-18");
258 assert_eq!(b.access_token, None);
259 assert_eq!(b.spec, None);
260 }
261
262 #[test]
263 fn test_builder_no_client() {
264 let b = EsiBuilder::new().user_agent("d").build().unwrap();
265
266 assert_eq!(b.client_id, None);
267 assert_eq!(b.client_secret, None);
268 assert_eq!(b.callback_url, None);
269 assert_eq!(b.base_api_url, "https://esi.evetech.net/");
270 assert_eq!(
271 b.authorize_url,
272 "https://login.eveonline.com/v2/oauth/authorize"
273 );
274 assert_eq!(b.token_url, "https://login.eveonline.com/v2/oauth/token");
275 assert_eq!(b.spec_url, "https://esi.evetech.net/meta/openapi.json");
276 assert_eq!(b.compatibility_date, "2026-08-18");
277 assert_eq!(b.access_token, None);
278 assert_eq!(b.spec, None);
279 }
280
281 #[test]
282 fn test_builder_change_urls() {
283 let b = EsiBuilder::new()
284 .user_agent("d")
285 .base_api_url("http://eve-api/")
286 .authorize_url("http://authorize-url/")
287 .token_url("http://token-url")
288 .spec_url("http://spec-url/")
289 .build()
290 .unwrap();
291
292 assert_eq!(b.base_api_url, "http://eve-api/");
293 assert_eq!(b.authorize_url, "http://authorize-url/");
294 assert_eq!(b.token_url, "http://token-url");
295 assert_eq!(b.spec_url, "http://spec-url/");
296 }
297
298 #[test]
299 fn test_builder_missing_value() {
300 let res = EsiBuilder::new().build();
301 assert!(res.is_err());
302 let s = format!("{}", res.unwrap_err());
303 assert_eq!(s, "Missing required builder struct value 'user_agent'");
304 }
305
306 #[test]
307 fn test_builder_with_spec() {
308 let spec: Spec = serde_json::from_str(r#"{"paths": {}}"#).unwrap();
309 let b = EsiBuilder::new()
310 .user_agent("d")
311 .spec(Some(spec.clone()))
312 .build()
313 .unwrap();
314
315 assert_eq!(spec, b.spec.unwrap());
316 }
317
318 #[test]
319 fn test_builder_to_json_empty() {
320 let json = r#"{"compatibility_date":null,"client_id":null,"client_secret":null,"application_auth":null,"callback_url":null,"base_api_url":null,"authorize_url":null,"token_url":null,"spec_url":null,"scope":null,"access_token":null,"access_expiration":null,"refresh_token":null,"user_agent":null,"http_timeout":null,"spec":null}"#;
321 assert_eq!(json, serde_json::to_string(&EsiBuilder::new()).unwrap());
322 }
323
324 #[test]
325 fn test_builder_from_json_filled() {
326 let json = r#"{
327 "compatibility_date": "2026-08-18",
328 "client_id": "a",
329 "client_secret": "b",
330 "callback_url": "c",
331 "scope": "d",
332 "access_token": "e",
333 "access_expiration": 1,
334 "refresh_token": "f",
335 "user_agent": "g",
336 "http_timeout": 60000,
337 "spec": null
338 }"#;
339 let actual: EsiBuilder = serde_json::from_str(json).unwrap();
340 let expected = EsiBuilder::new()
341 .compatibility_date("2026-08-18")
342 .client_id("a")
343 .client_secret("b")
344 .callback_url("c")
345 .scope("d")
346 .access_token(Some("e"))
347 .access_expiration(Some(1))
348 .refresh_token(Some("f"))
349 .user_agent("g")
350 .http_timeout(Some(60_000))
351 .spec(None);
352
353 assert_eq!(actual, expected);
354 }
355}