Skip to main content

esi_openapi/
builders.rs

1//! Builders
2
3use crate::{prelude::*, spec::Spec};
4use reqwest::{header, Client};
5use std::time::Duration;
6
7/// Builder for the `Esi` struct.
8///
9/// # Example
10///
11/// ```rust
12/// # use esi_openapi::prelude::EsiBuilder;
13/// let mut esi = EsiBuilder::new()
14///     .user_agent("some user agent")
15///     .client_id("your_client_id")
16///     .client_secret("your_client_secret")
17///     .callback_url("your_callback_url")
18///     .build()
19///     .unwrap();
20/// ```
21///
22/// # Overriding the API specification
23///
24/// If you will be creating multiple struct instances,
25/// you will probably run into the issue of needing to
26/// retrieve the API spec on each. Given that this
27/// operation is fairly expensive (certainly as far as
28/// the rest of the API endpoints that EVE makes available),
29/// this builder supports setting that struct:
30///
31/// ```rust
32/// # use esi_openapi::prelude::EsiBuilder;
33/// # let your_spec = serde_json::from_str(r#"{"paths": {}}"#).unwrap();
34/// let mut esi = EsiBuilder::new()
35///     .user_agent("some user agent")
36///     .spec(Some(your_spec))
37///     .build()
38///     .unwrap();
39/// ```
40///
41/// Note that this "spec" function is just another builder
42/// function; you can make use it alongside all of the others.
43/// Note also that this is entirely optional: if you don't
44/// mind retrieving the spec from ESI on each of your
45/// struct instances, you can safely ignore this.
46///
47/// # Not including client info
48///
49/// If you are only making calls to non-authenticated
50/// endpoints, then you don't need to make use of
51/// the authentication flow, which means you don't need
52/// a client ID, client secret, and callback URL.
53/// In this case, you can construct your client without
54/// those parameters:
55///
56/// ```rust
57/// # use esi_openapi::prelude::EsiBuilder;
58/// let mut esi = EsiBuilder::new()
59///     .user_agent("some user agent")
60///     .build()
61///     .unwrap();
62/// ```
63///
64/// Note that you still need to set the user agent, as this is good
65/// API usage behavior.
66#[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    /// Start a new builder.
88    pub fn new() -> Self {
89        Default::default()
90    }
91
92    /// Set the compatibility header to use.
93    ///
94    /// Will default to a hardcoded value if not set.
95    pub fn compatibility_date(mut self, val: &str) -> Self {
96        self.compatibility_date = Some(val.to_owned());
97        self
98    }
99
100    /// Set the client_id.
101    pub fn client_id(mut self, val: &str) -> Self {
102        self.client_id = Some(val.to_owned());
103        self
104    }
105
106    /// Set the client_secret (https://docs.esi.evetech.net/docs/sso/web_based_sso_flow.html).
107    pub fn client_secret(mut self, val: &str) -> Self {
108        self.client_secret = Some(val.to_owned());
109        self
110    }
111
112    /// Enable PKCE Authentication flow for Applications (https://docs.esi.evetech.net/docs/sso/native_sso_flow.html)
113    pub fn enable_application_authentication(mut self, val: bool) -> Self {
114        self.application_auth = Some(val);
115        self
116    }
117
118    /// Set the callback_url.
119    pub fn callback_url(mut self, val: &str) -> Self {
120        self.callback_url = Some(val.to_owned());
121        self
122    }
123
124    /// Set the base_api_url.
125    pub fn base_api_url(mut self, val: &str) -> Self {
126        self.base_api_url = Some(val.to_owned());
127        self
128    }
129
130    /// Set the authorize_url.
131    pub fn authorize_url(mut self, val: &str) -> Self {
132        self.authorize_url = Some(val.to_owned());
133        self
134    }
135
136    /// Set the token_url.
137    pub fn token_url(mut self, val: &str) -> Self {
138        self.token_url = Some(val.to_owned());
139        self
140    }
141
142    /// Set the spec_url.
143    pub fn spec_url(mut self, val: &str) -> Self {
144        self.spec_url = Some(val.to_owned());
145        self
146    }
147
148    /// Set the scope.
149    pub fn scope(mut self, val: &str) -> Self {
150        self.scope = Some(val.to_owned().replace(' ', "%20"));
151        self
152    }
153
154    /// Set the access_token.
155    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    /// Set the access_expiration.
161    pub fn access_expiration(mut self, val: Option<i64>) -> Self {
162        self.access_expiration = val;
163        self
164    }
165
166    /// Set the refresh_token.
167    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    /// Set the user_agent.
173    pub fn user_agent(mut self, val: &str) -> Self {
174        self.user_agent = Some(val.to_owned());
175        self
176    }
177
178    /// Set the timeout to use in millis when sending HTTP requests.
179    ///
180    /// Will default to 60,000 (1 minute) if not set.
181    pub fn http_timeout(mut self, val: Option<u64>) -> Self {
182        self.http_timeout = val;
183        self
184    }
185
186    /// Explicitly set the OpenAPI specification.
187    ///
188    /// Allows copying the spec from another `Esi` struct
189    /// or other source, avoiding fetching the spec from
190    /// ESI again. May be useful given the runtime cost
191    /// of retrieving the spec.
192    ///
193    /// Be aware of the potential for out-of-date data.
194    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    /// Construct the `Esi` instance.
230    ///
231    /// There are a few things that could go wrong, like
232    /// not setting one of the mandatory fields or providing a user
233    /// agent that is not a valid HTTP header value.
234    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}