1use std::sync::Arc;
2use std::time::Duration;
3
4use crate::{
5 auth::Auth,
6 corporate_actions::CorporateActionsClient,
7 crypto::CryptoClient,
8 env,
9 error::Error,
10 news::NewsClient,
11 options::OptionsClient,
12 stocks::StocksClient,
13 transport::{
14 http::HttpClient,
15 observer::{ObserverHandle, TransportObserver},
16 rate_limit::RateLimiter,
17 retry::RetryConfig,
18 },
19};
20
21const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
22
23#[derive(Clone, Debug)]
39pub struct Client {
40 pub(crate) inner: Arc<Inner>,
41}
42
43#[allow(dead_code)]
44#[derive(Debug)]
45pub(crate) struct Inner {
46 pub(crate) auth: Auth,
47 pub(crate) base_url: String,
48 pub(crate) timeout: Option<Duration>,
49 pub(crate) retry_config: RetryConfig,
50 pub(crate) max_in_flight: Option<usize>,
51 pub(crate) http: HttpClient,
52}
53
54#[derive(Clone, Debug)]
55pub struct ClientBuilder {
56 api_key: Option<String>,
57 secret_key: Option<String>,
58 base_url: Option<String>,
59 timeout: Option<Duration>,
60 reqwest_client: Option<reqwest::Client>,
61 observer: Option<ObserverHandle>,
62 retry_config: RetryConfig,
63 max_in_flight: Option<usize>,
64}
65
66impl Client {
67 pub fn new() -> Self {
71 Self::builder()
72 .build()
73 .expect("client builder is infallible during bootstrap")
74 }
75
76 pub fn builder() -> ClientBuilder {
78 ClientBuilder::default()
79 }
80
81 pub fn stocks(&self) -> StocksClient {
83 StocksClient::new(self.inner.clone())
84 }
85
86 pub fn options(&self) -> OptionsClient {
88 OptionsClient::new(self.inner.clone())
89 }
90
91 pub fn crypto(&self) -> CryptoClient {
93 CryptoClient::new(self.inner.clone())
94 }
95
96 pub fn news(&self) -> NewsClient {
98 NewsClient::new(self.inner.clone())
99 }
100
101 pub fn corporate_actions(&self) -> CorporateActionsClient {
103 CorporateActionsClient::new(self.inner.clone())
104 }
105
106 pub(crate) fn from_parts(
107 auth: Auth,
108 base_url: String,
109 timeout: Option<Duration>,
110 reqwest_client: Option<reqwest::Client>,
111 observer: Option<ObserverHandle>,
112 retry_config: RetryConfig,
113 max_in_flight: Option<usize>,
114 ) -> Result<Self, Error> {
115 let http = match reqwest_client {
116 Some(client) => HttpClient::with_client(
117 client,
118 observer,
119 retry_config.clone(),
120 RateLimiter::new(max_in_flight),
121 ),
122 None => HttpClient::from_timeout(
123 timeout.unwrap_or(DEFAULT_TIMEOUT),
124 observer,
125 retry_config.clone(),
126 RateLimiter::new(max_in_flight),
127 )?,
128 };
129
130 Ok(Self {
131 inner: Arc::new(Inner {
132 auth,
133 base_url,
134 timeout,
135 retry_config,
136 max_in_flight,
137 http,
138 }),
139 })
140 }
141}
142
143impl Default for ClientBuilder {
144 fn default() -> Self {
145 Self {
146 api_key: None,
147 secret_key: None,
148 base_url: None,
149 timeout: None,
150 reqwest_client: None,
151 observer: None,
152 retry_config: RetryConfig::default(),
153 max_in_flight: None,
154 }
155 }
156}
157
158impl ClientBuilder {
159 pub fn api_key(mut self, api_key: impl Into<String>) -> Self {
161 self.api_key = Some(api_key.into());
162 self
163 }
164
165 pub fn secret_key(mut self, secret_key: impl Into<String>) -> Self {
167 self.secret_key = Some(secret_key.into());
168 self
169 }
170
171 pub fn base_url(mut self, base_url: impl Into<String>) -> Self {
173 self.base_url = Some(base_url.into());
174 self
175 }
176
177 pub fn timeout(mut self, timeout: Duration) -> Self {
182 self.timeout = Some(timeout);
183 self
184 }
185
186 pub fn reqwest_client(mut self, reqwest_client: reqwest::Client) -> Self {
192 self.reqwest_client = Some(reqwest_client);
193 self
194 }
195
196 pub fn observer(mut self, observer: Arc<dyn TransportObserver>) -> Self {
201 self.observer = Some(ObserverHandle::new(observer));
202 self
203 }
204
205 pub fn max_retries(mut self, max_retries: u32) -> Self {
210 self.retry_config.max_retries = max_retries;
211 self
212 }
213
214 pub fn retry_on_429(mut self, retry_on_429: bool) -> Self {
219 self.retry_config.retry_on_429 = retry_on_429;
220 self
221 }
222
223 pub fn respect_retry_after(mut self, respect_retry_after: bool) -> Self {
229 self.retry_config.respect_retry_after = respect_retry_after;
230 self
231 }
232
233 pub fn base_backoff(mut self, base_backoff: Duration) -> Self {
235 self.retry_config.base_backoff = base_backoff;
236 self
237 }
238
239 pub fn max_backoff(mut self, max_backoff: Duration) -> Self {
241 self.retry_config.max_backoff = max_backoff;
242 self
243 }
244
245 pub fn retry_jitter(mut self, retry_jitter: Duration) -> Self {
251 self.retry_config.jitter = Some(retry_jitter);
252 self
253 }
254
255 pub fn total_retry_budget(mut self, total_retry_budget: Duration) -> Self {
262 self.retry_config.total_retry_budget = Some(total_retry_budget);
263 self
264 }
265
266 pub fn credentials_from_env(self) -> Result<Self, Error> {
271 self.credentials_from_env_names(env::DEFAULT_API_KEY_ENV, env::DEFAULT_SECRET_KEY_ENV)
272 }
273
274 pub fn credentials_from_env_names(
279 mut self,
280 api_key_var: &str,
281 secret_key_var: &str,
282 ) -> Result<Self, Error> {
283 if let Some((api_key, secret_key)) =
284 env::credentials_from_env_names(api_key_var, secret_key_var)?
285 {
286 self.api_key = Some(api_key);
287 self.secret_key = Some(secret_key);
288 }
289
290 Ok(self)
291 }
292
293 pub fn max_in_flight(mut self, max_in_flight: usize) -> Self {
295 self.max_in_flight = Some(max_in_flight);
296 self
297 }
298
299 pub fn build(self) -> Result<Client, Error> {
303 if self.retry_config.max_backoff < self.retry_config.base_backoff {
304 return Err(Error::InvalidConfiguration(
305 "max_backoff must be greater than or equal to base_backoff".into(),
306 ));
307 }
308
309 if self.reqwest_client.is_some() && self.timeout.is_some() {
310 return Err(Error::InvalidConfiguration(
311 "reqwest_client owns timeout configuration; remove timeout(...) or configure timeout on the injected reqwest::Client".into(),
312 ));
313 }
314
315 let auth = Auth::new(self.api_key, self.secret_key)?;
316 let base_url = self
317 .base_url
318 .unwrap_or_else(|| "https://data.alpaca.markets".to_string());
319
320 Client::from_parts(
321 auth,
322 base_url,
323 self.timeout,
324 self.reqwest_client,
325 self.observer,
326 self.retry_config,
327 self.max_in_flight,
328 )
329 }
330}