Skip to main content

tradestation_api/
client.rs

1//! TradeStation API client with automatic token management.
2//!
3//! The [`Client`] handles OAuth2 token lifecycle (exchange, refresh, expiration)
4//! and provides authenticated HTTP methods for all API endpoints.
5
6use reqwest::header::{self, HeaderMap, HeaderValue};
7
8use crate::Error;
9use crate::auth::{self, Credentials, Token};
10
11const BASE_URL: &str = "https://api.tradestation.com";
12const SIM_URL: &str = "https://sim-api.tradestation.com";
13
14/// TradeStation API client with automatic token management.
15///
16/// # Example
17///
18/// ```no_run
19/// use tradestation_api::{Client, Credentials};
20///
21/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
22/// let creds = Credentials::new("id", "secret");
23/// let mut client = Client::new(creds);
24/// client.authenticate("auth_code").await?;
25///
26/// let accounts = client.get_accounts().await?;
27/// # Ok(())
28/// # }
29/// ```
30pub struct Client {
31    pub(crate) http: reqwest::Client,
32    credentials: Credentials,
33    token: Option<Token>,
34    base_url: String,
35}
36
37impl Client {
38    /// Create a new client targeting the production API.
39    pub fn new(credentials: Credentials) -> Self {
40        Self {
41            http: reqwest::Client::new(),
42            credentials,
43            token: None,
44            base_url: BASE_URL.to_string(),
45        }
46    }
47
48    /// Use the simulation API instead of production.
49    pub fn with_sim(mut self) -> Self {
50        self.base_url = SIM_URL.to_string();
51        self
52    }
53
54    /// Override the base URL (useful for testing with mock servers).
55    pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
56        self.base_url = url.into();
57        self
58    }
59
60    /// Set a pre-existing token (e.g., loaded from persistent storage).
61    pub fn with_token(mut self, token: Token) -> Self {
62        self.token = Some(token);
63        self
64    }
65
66    /// Exchange an authorization code for tokens, completing the OAuth2 flow.
67    pub async fn authenticate(&mut self, code: &str) -> Result<&Token, Error> {
68        let token = auth::exchange_code(&self.http, &self.credentials, code).await?;
69        self.token = Some(token);
70        Ok(self.token.as_ref().unwrap())
71    }
72
73    /// Get a valid access token, automatically refreshing if expired.
74    pub async fn access_token(&mut self) -> Result<String, Error> {
75        let token = self
76            .token
77            .as_ref()
78            .ok_or_else(|| Error::Auth("Not authenticated".to_string()))?;
79
80        if !token.is_expired() {
81            return Ok(token.access_token.clone());
82        }
83
84        // Need to refresh
85        if let Some(refresh_tok) = &token.refresh_token
86            && !token.refresh_expired()
87        {
88            let new_token = auth::refresh_token(&self.http, &self.credentials, refresh_tok).await?;
89            self.token = Some(new_token);
90            return Ok(self.token.as_ref().unwrap().access_token.clone());
91        }
92
93        Err(Error::Auth(
94            "Token expired and cannot be refreshed — re-authenticate".to_string(),
95        ))
96    }
97
98    /// Build authenticated headers with the current bearer token.
99    pub(crate) async fn auth_headers(&mut self) -> Result<HeaderMap, Error> {
100        let token = self.access_token().await?;
101        let mut headers = HeaderMap::new();
102        headers.insert(
103            header::AUTHORIZATION,
104            HeaderValue::from_str(&format!("Bearer {token}"))
105                .map_err(|e| Error::Auth(e.to_string()))?,
106        );
107        Ok(headers)
108    }
109
110    /// Make an authenticated GET request to the given path.
111    pub async fn get(&mut self, path: &str) -> Result<reqwest::Response, Error> {
112        let headers = self.auth_headers().await?;
113        let url = format!("{}{}", self.base_url, path);
114        let resp = self.http.get(&url).headers(headers).send().await?;
115
116        if !resp.status().is_success() {
117            let status = resp.status().as_u16();
118            let body = resp.text().await.unwrap_or_default();
119            return Err(Error::Api {
120                status,
121                message: body,
122            });
123        }
124        Ok(resp)
125    }
126
127    /// Make an authenticated POST request with a JSON body.
128    pub async fn post<T: serde::Serialize>(
129        &mut self,
130        path: &str,
131        body: &T,
132    ) -> Result<reqwest::Response, Error> {
133        let headers = self.auth_headers().await?;
134        let url = format!("{}{}", self.base_url, path);
135        let resp = self
136            .http
137            .post(&url)
138            .headers(headers)
139            .json(body)
140            .send()
141            .await?;
142
143        if !resp.status().is_success() {
144            let status = resp.status().as_u16();
145            let body = resp.text().await.unwrap_or_default();
146            return Err(Error::Api {
147                status,
148                message: body,
149            });
150        }
151        Ok(resp)
152    }
153
154    /// Make an authenticated DELETE request.
155    pub async fn delete(&mut self, path: &str) -> Result<reqwest::Response, Error> {
156        let headers = self.auth_headers().await?;
157        let url = format!("{}{}", self.base_url, path);
158        let resp = self.http.delete(&url).headers(headers).send().await?;
159
160        if !resp.status().is_success() {
161            let status = resp.status().as_u16();
162            let body = resp.text().await.unwrap_or_default();
163            return Err(Error::Api {
164                status,
165                message: body,
166            });
167        }
168        Ok(resp)
169    }
170
171    /// Make an authenticated PUT request with a JSON body.
172    pub async fn put<T: serde::Serialize>(
173        &mut self,
174        path: &str,
175        body: &T,
176    ) -> Result<reqwest::Response, Error> {
177        let headers = self.auth_headers().await?;
178        let url = format!("{}{}", self.base_url, path);
179        let resp = self
180            .http
181            .put(&url)
182            .headers(headers)
183            .json(body)
184            .send()
185            .await?;
186
187        if !resp.status().is_success() {
188            let status = resp.status().as_u16();
189            let body = resp.text().await.unwrap_or_default();
190            return Err(Error::Api {
191                status,
192                message: body,
193            });
194        }
195        Ok(resp)
196    }
197
198    /// Get the current token (for status display or persistence).
199    pub fn token_info(&self) -> Option<&Token> {
200        self.token.as_ref()
201    }
202
203    /// Get the current base URL.
204    pub fn base_url(&self) -> &str {
205        &self.base_url
206    }
207}