use reqwest::header::{self, HeaderMap, HeaderValue};
use crate::Error;
use crate::auth::{self, Credentials, Token};
const BASE_URL: &str = "https://api.tradestation.com";
const SIM_URL: &str = "https://sim-api.tradestation.com";
pub struct Client {
pub(crate) http: reqwest::Client,
credentials: Credentials,
token: Option<Token>,
base_url: String,
}
impl Client {
pub fn new(credentials: Credentials) -> Self {
Self {
http: reqwest::Client::new(),
credentials,
token: None,
base_url: BASE_URL.to_string(),
}
}
pub fn with_sim(mut self) -> Self {
self.base_url = SIM_URL.to_string();
self
}
pub fn with_base_url(mut self, url: impl Into<String>) -> Self {
self.base_url = url.into();
self
}
pub fn with_token(mut self, token: Token) -> Self {
self.token = Some(token);
self
}
pub async fn authenticate(&mut self, code: &str) -> Result<&Token, Error> {
let token = auth::exchange_code(&self.http, &self.credentials, code).await?;
self.token = Some(token);
Ok(self.token.as_ref().unwrap())
}
pub async fn access_token(&mut self) -> Result<String, Error> {
let token = self
.token
.as_ref()
.ok_or_else(|| Error::Auth("Not authenticated".to_string()))?;
if !token.is_expired() {
return Ok(token.access_token.clone());
}
if let Some(refresh_tok) = &token.refresh_token
&& !token.refresh_expired()
{
let new_token = auth::refresh_token(&self.http, &self.credentials, refresh_tok).await?;
self.token = Some(new_token);
return Ok(self.token.as_ref().unwrap().access_token.clone());
}
Err(Error::Auth(
"Token expired and cannot be refreshed — re-authenticate".to_string(),
))
}
pub(crate) async fn auth_headers(&mut self) -> Result<HeaderMap, Error> {
let token = self.access_token().await?;
let mut headers = HeaderMap::new();
headers.insert(
header::AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {token}"))
.map_err(|e| Error::Auth(e.to_string()))?,
);
Ok(headers)
}
pub async fn get(&mut self, path: &str) -> Result<reqwest::Response, Error> {
let headers = self.auth_headers().await?;
let url = format!("{}{}", self.base_url, path);
let resp = self.http.get(&url).headers(headers).send().await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(Error::Api {
status,
message: body,
});
}
Ok(resp)
}
pub async fn post<T: serde::Serialize>(
&mut self,
path: &str,
body: &T,
) -> Result<reqwest::Response, Error> {
let headers = self.auth_headers().await?;
let url = format!("{}{}", self.base_url, path);
let resp = self
.http
.post(&url)
.headers(headers)
.json(body)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(Error::Api {
status,
message: body,
});
}
Ok(resp)
}
pub async fn delete(&mut self, path: &str) -> Result<reqwest::Response, Error> {
let headers = self.auth_headers().await?;
let url = format!("{}{}", self.base_url, path);
let resp = self.http.delete(&url).headers(headers).send().await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(Error::Api {
status,
message: body,
});
}
Ok(resp)
}
pub async fn put<T: serde::Serialize>(
&mut self,
path: &str,
body: &T,
) -> Result<reqwest::Response, Error> {
let headers = self.auth_headers().await?;
let url = format!("{}{}", self.base_url, path);
let resp = self
.http
.put(&url)
.headers(headers)
.json(body)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status().as_u16();
let body = resp.text().await.unwrap_or_default();
return Err(Error::Api {
status,
message: body,
});
}
Ok(resp)
}
pub fn token_info(&self) -> Option<&Token> {
self.token.as_ref()
}
pub fn base_url(&self) -> &str {
&self.base_url
}
}