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 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub(crate) cache_enabled: Option<bool>,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub(crate) cache_max_entries: Option<usize>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub(crate) cache_max_bytes: Option<usize>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub(crate) rate_limit_policy: Option<RateLimitPolicy>,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub(crate) page_concurrency: Option<usize>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 pub(crate) language: Option<Language>,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub(crate) tenant: Option<String>,
97 pub(crate) spec: Option<Spec>,
98}
99
100#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
102#[serde(rename_all = "lowercase")]
103pub enum Language {
104 #[default]
106 En,
107 De,
109 Fr,
111 Ja,
113 Ru,
115 Zh,
117 Ko,
119 Es,
121}
122
123impl Language {
124 pub fn as_str(&self) -> &'static str {
126 match self {
127 Language::En => "en",
128 Language::De => "de",
129 Language::Fr => "fr",
130 Language::Ja => "ja",
131 Language::Ru => "ru",
132 Language::Zh => "zh",
133 Language::Ko => "ko",
134 Language::Es => "es",
135 }
136 }
137}
138
139impl EsiBuilder {
140 pub fn new() -> Self {
142 Default::default()
143 }
144
145 pub fn compatibility_date(mut self, val: &str) -> Self {
149 self.compatibility_date = Some(val.to_owned());
150 self
151 }
152
153 pub fn client_id(mut self, val: &str) -> Self {
155 self.client_id = Some(val.to_owned());
156 self
157 }
158
159 pub fn client_secret(mut self, val: &str) -> Self {
161 self.client_secret = Some(val.to_owned());
162 self
163 }
164
165 pub fn enable_application_authentication(mut self, val: bool) -> Self {
167 self.application_auth = Some(val);
168 self
169 }
170
171 pub fn callback_url(mut self, val: &str) -> Self {
173 self.callback_url = Some(val.to_owned());
174 self
175 }
176
177 pub fn base_api_url(mut self, val: &str) -> Self {
179 self.base_api_url = Some(val.to_owned());
180 self
181 }
182
183 pub fn authorize_url(mut self, val: &str) -> Self {
185 self.authorize_url = Some(val.to_owned());
186 self
187 }
188
189 pub fn token_url(mut self, val: &str) -> Self {
191 self.token_url = Some(val.to_owned());
192 self
193 }
194
195 pub fn spec_url(mut self, val: &str) -> Self {
197 self.spec_url = Some(val.to_owned());
198 self
199 }
200
201 pub fn scope(mut self, val: &str) -> Self {
203 self.scope = Some(val.to_owned().replace(' ', "%20"));
204 self
205 }
206
207 pub fn access_token(mut self, val: Option<&str>) -> Self {
209 self.access_token = val.map(|v| v.to_owned());
210 self
211 }
212
213 pub fn access_expiration(mut self, val: Option<i64>) -> Self {
215 self.access_expiration = val;
216 self
217 }
218
219 pub fn refresh_token(mut self, val: Option<&str>) -> Self {
221 self.refresh_token = val.map(|v| v.to_owned());
222 self
223 }
224
225 pub fn user_agent(mut self, val: &str) -> Self {
227 self.user_agent = Some(val.to_owned());
228 self
229 }
230
231 pub fn http_timeout(mut self, val: Option<u64>) -> Self {
235 self.http_timeout = val;
236 self
237 }
238
239 pub fn spec(mut self, spec: Option<Spec>) -> Self {
248 self.spec = spec;
249 self
250 }
251
252 pub(crate) fn construct_client(&self) -> EsiResult<Client> {
253 let http_timeout = self
254 .http_timeout
255 .map(Duration::from_millis)
256 .unwrap_or_else(|| Duration::from_secs(60));
257 let headers = {
258 let mut map = header::HeaderMap::new();
259 let user_agent = &self
260 .user_agent
261 .as_ref()
262 .ok_or_else(|| EsiError::EmptyClientValue("user_agent".to_owned()))?
263 .to_owned();
264 map.insert(
265 header::USER_AGENT,
266 header::HeaderValue::from_str(user_agent)?,
267 );
268 map.insert(
269 header::ACCEPT,
270 header::HeaderValue::from_static("application/json"),
271 );
272 map
273 };
274 let builder = Client::builder()
275 .timeout(http_timeout)
276 .default_headers(headers);
277 #[cfg(feature = "rustls-tls")]
278 let builder = builder.tls_backend_rustls();
279 Ok(builder.build()?)
280 }
281
282 pub fn cache_max_entries(mut self, val: usize) -> Self {
286 self.cache_max_entries = Some(val);
287 self
288 }
289
290 pub fn cache_max_bytes(mut self, val: usize) -> Self {
297 self.cache_max_bytes = Some(val);
298 self
299 }
300
301 pub fn rate_limit_policy(mut self, val: RateLimitPolicy) -> Self {
309 self.rate_limit_policy = Some(val);
310 self
311 }
312
313 pub fn page_concurrency(mut self, val: usize) -> Self {
316 self.page_concurrency = Some(val);
317 self
318 }
319
320 pub fn language(mut self, val: Language) -> Self {
324 self.language = Some(val);
325 self
326 }
327
328 pub fn tenant(mut self, val: &str) -> Self {
331 self.tenant = Some(val.to_owned());
332 self
333 }
334
335 pub fn enable_cache(mut self, val: bool) -> Self {
343 self.cache_enabled = Some(val);
344 self
345 }
346
347 pub fn build(self) -> EsiResult<Esi> {
353 Esi::from_builder(self)
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::EsiBuilder;
360 use crate::spec::Spec;
361
362 #[test]
363 fn test_language_and_tenant() {
364 let esi = EsiBuilder::new()
365 .user_agent("d")
366 .language(super::Language::De)
367 .tenant("singularity")
368 .build()
369 .unwrap();
370 assert_eq!(esi.language, Some(super::Language::De));
371 assert_eq!(esi.tenant.as_deref(), Some("singularity"));
372 assert_eq!(super::Language::Ko.as_str(), "ko");
373 let json = serde_json::to_string(&EsiBuilder::new().language(super::Language::Fr)).unwrap();
374 assert!(json.contains("\"language\":\"fr\""));
375 }
376
377 #[test]
378 fn test_builder_valid() {
379 let b = EsiBuilder::new()
380 .client_id("a")
381 .client_secret("b")
382 .callback_url("c")
383 .user_agent("d")
384 .build()
385 .unwrap();
386
387 assert_eq!(b.client_id, Some(String::from("a")));
388 assert_eq!(b.client_secret, Some(String::from("b")));
389 assert_eq!(b.callback_url, Some(String::from("c")));
390 assert_eq!(b.compatibility_date, "2026-08-18");
391 assert_eq!(b.access_token, None);
392 assert_eq!(b.spec, None);
393 }
394
395 #[test]
396 fn test_builder_no_client() {
397 let b = EsiBuilder::new().user_agent("d").build().unwrap();
398
399 assert_eq!(b.client_id, None);
400 assert_eq!(b.client_secret, None);
401 assert_eq!(b.callback_url, None);
402 assert_eq!(b.base_api_url, "https://esi.evetech.net/");
403 assert_eq!(
404 b.authorize_url,
405 "https://login.eveonline.com/v2/oauth/authorize"
406 );
407 assert_eq!(b.token_url, "https://login.eveonline.com/v2/oauth/token");
408 assert_eq!(b.spec_url, "https://esi.evetech.net/meta/openapi.json");
409 assert_eq!(b.compatibility_date, "2026-08-18");
410 assert_eq!(b.access_token, None);
411 assert_eq!(b.spec, None);
412 }
413
414 #[test]
415 fn test_builder_change_urls() {
416 let b = EsiBuilder::new()
417 .user_agent("d")
418 .base_api_url("http://eve-api/")
419 .authorize_url("http://authorize-url/")
420 .token_url("http://token-url")
421 .spec_url("http://spec-url/")
422 .build()
423 .unwrap();
424
425 assert_eq!(b.base_api_url, "http://eve-api/");
426 assert_eq!(b.authorize_url, "http://authorize-url/");
427 assert_eq!(b.token_url, "http://token-url");
428 assert_eq!(b.spec_url, "http://spec-url/");
429 }
430
431 #[test]
432 fn test_builder_missing_value() {
433 let res = EsiBuilder::new().build();
434 assert!(res.is_err());
435 let s = format!("{}", res.unwrap_err());
436 assert_eq!(s, "Missing required builder struct value 'user_agent'");
437 }
438
439 #[test]
440 fn test_builder_with_spec() {
441 let spec: Spec = serde_json::from_str(r#"{"paths": {}}"#).unwrap();
442 let b = EsiBuilder::new()
443 .user_agent("d")
444 .spec(Some(spec.clone()))
445 .build()
446 .unwrap();
447
448 assert_eq!(spec, b.spec.unwrap());
449 }
450
451 #[test]
452 fn test_builder_to_json_empty() {
453 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}"#;
454 assert_eq!(json, serde_json::to_string(&EsiBuilder::new()).unwrap());
455 }
456
457 #[test]
458 fn test_builder_from_json_filled() {
459 let json = r#"{
460 "compatibility_date": "2026-08-18",
461 "client_id": "a",
462 "client_secret": "b",
463 "callback_url": "c",
464 "scope": "d",
465 "access_token": "e",
466 "access_expiration": 1,
467 "refresh_token": "f",
468 "user_agent": "g",
469 "http_timeout": 60000,
470 "spec": null
471 }"#;
472 let actual: EsiBuilder = serde_json::from_str(json).unwrap();
473 let expected = EsiBuilder::new()
474 .compatibility_date("2026-08-18")
475 .client_id("a")
476 .client_secret("b")
477 .callback_url("c")
478 .scope("d")
479 .access_token(Some("e"))
480 .access_expiration(Some(1))
481 .refresh_token(Some("f"))
482 .user_agent("g")
483 .http_timeout(Some(60_000))
484 .spec(None);
485
486 assert_eq!(actual, expected);
487 }
488}