tradestation_api/
client.rs1use 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
14pub struct Client {
31 pub(crate) http: reqwest::Client,
32 credentials: Credentials,
33 token: Option<Token>,
34 base_url: String,
35}
36
37impl Client {
38 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 pub fn with_sim(mut self) -> Self {
50 self.base_url = SIM_URL.to_string();
51 self
52 }
53
54 pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
56 self.base_url = url.into();
57 self
58 }
59
60 pub fn with_token(mut self, token: Token) -> Self {
62 self.token = Some(token);
63 self
64 }
65
66 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 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 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 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 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 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 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 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 pub fn token_info(&self) -> Option<&Token> {
200 self.token.as_ref()
201 }
202
203 pub fn base_url(&self) -> &str {
205 &self.base_url
206 }
207}