use reqwest::Client;
use std::time::Duration;
use thiserror::Error;
const FLEX_BASE_URL: &str = "https://gdcdyn.interactivebrokers.com/Universal/servlet";
#[derive(Debug, Error)]
pub enum FlexApiError {
#[error("HTTP request failed: {0}")]
RequestFailed(#[from] reqwest::Error),
#[error("IB API error: {0}")]
ApiError(String),
#[error("XML parsing error: {0}")]
XmlError(String),
#[error("Statement not ready (try again later)")]
StatementNotReady,
#[error("Invalid response format: {0}")]
InvalidResponse(String),
}
pub type Result<T> = std::result::Result<T, FlexApiError>;
#[derive(Debug, Clone)]
pub struct FlexApiClient {
token: String,
base_url: String,
client: Client,
}
impl FlexApiClient {
pub fn new(token: impl Into<String>) -> Self {
Self {
token: token.into(),
base_url: FLEX_BASE_URL.to_string(),
client: Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("Failed to build HTTP client"),
}
}
pub fn with_base_url(token: impl Into<String>, base_url: impl Into<String>) -> Self {
Self {
token: token.into(),
base_url: base_url.into(),
client: Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("Failed to build HTTP client"),
}
}
pub async fn send_request(&self, query_id: &str) -> Result<String> {
let url = format!(
"{}/FlexStatementService.SendRequest?t={}&q={}&v=3",
self.base_url, self.token, query_id
);
let response = self.client.get(&url).send().await?;
let body = response.text().await?;
self.parse_send_request_response(&body)
}
pub async fn get_statement(&self, reference_code: &str) -> Result<String> {
let url = format!(
"{}/FlexStatementService.GetStatement?t={}&q={}&v=3",
self.base_url, self.token, reference_code
);
let response = self.client.get(&url).send().await?;
let body = response.text().await?;
if body.contains("<Status>") {
self.parse_get_statement_response(&body)
} else {
Ok(body)
}
}
pub async fn get_statement_with_retry(
&self,
reference_code: &str,
max_retries: usize,
retry_delay: Duration,
) -> Result<String> {
for attempt in 0..=max_retries {
match self.get_statement(reference_code).await {
Ok(xml) => return Ok(xml),
Err(FlexApiError::StatementNotReady) => {
if attempt < max_retries {
tokio::time::sleep(retry_delay).await;
continue;
} else {
return Err(FlexApiError::StatementNotReady);
}
}
Err(e) => return Err(e),
}
}
unreachable!("Loop should always return within the iteration")
}
fn parse_send_request_response(&self, xml: &str) -> Result<String> {
if let Some(start) = xml.find("<ReferenceCode>") {
if let Some(end) = xml[start..].find("</ReferenceCode>") {
let ref_code = &xml[start + 15..start + end];
return Ok(ref_code.to_string());
}
}
if xml.contains("<Status>Fail</Status>") || xml.contains("<Status>Warn</Status>") {
if let Some(start) = xml.find("<ErrorMessage>") {
if let Some(end) = xml[start..].find("</ErrorMessage>") {
let error = &xml[start + 14..start + end];
return Err(FlexApiError::ApiError(error.to_string()));
}
}
return Err(FlexApiError::ApiError("Unknown error".to_string()));
}
Err(FlexApiError::InvalidResponse(
"Could not parse reference code".to_string(),
))
}
fn parse_get_statement_response(&self, xml: &str) -> Result<String> {
if xml.contains("<ErrorCode>1019</ErrorCode>") {
return Err(FlexApiError::StatementNotReady);
}
if xml.contains("<Status>Fail</Status>") || xml.contains("<Status>Warn</Status>") {
if let Some(start) = xml.find("<ErrorMessage>") {
if let Some(end) = xml[start..].find("</ErrorMessage>") {
let error = &xml[start + 14..start + end];
return Err(FlexApiError::ApiError(error.to_string()));
}
}
return Err(FlexApiError::ApiError("Unknown error".to_string()));
}
Err(FlexApiError::InvalidResponse(
"Unexpected response format".to_string(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_send_request_success() {
let client = FlexApiClient::new("test_token");
let xml = r#"
<FlexStatementResponse timestamp='01 January, 2025 12:00 AM EDT'>
<Status>Success</Status>
<ReferenceCode>1234567890</ReferenceCode>
<Url>https://example.com</Url>
</FlexStatementResponse>
"#;
let result = client.parse_send_request_response(xml);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "1234567890");
}
#[test]
fn test_parse_send_request_error() {
let client = FlexApiClient::new("test_token");
let xml = r#"
<FlexStatementResponse timestamp='01 January, 2025 12:00 AM EDT'>
<Status>Fail</Status>
<ErrorCode>1003</ErrorCode>
<ErrorMessage>Invalid token</ErrorMessage>
</FlexStatementResponse>
"#;
let result = client.parse_send_request_response(xml);
assert!(result.is_err());
match result {
Err(FlexApiError::ApiError(msg)) => assert_eq!(msg, "Invalid token"),
_ => panic!("Expected ApiError"),
}
}
#[test]
fn test_parse_get_statement_not_ready() {
let client = FlexApiClient::new("test_token");
let xml = r#"
<FlexStatementResponse timestamp='01 January, 2025 12:00 AM EDT'>
<Status>Warn</Status>
<ErrorCode>1019</ErrorCode>
<ErrorMessage>Statement is being generated; please try again shortly</ErrorMessage>
</FlexStatementResponse>
"#;
let result = client.parse_get_statement_response(xml);
assert!(result.is_err());
match result {
Err(FlexApiError::StatementNotReady) => (),
_ => panic!("Expected StatementNotReady"),
}
}
#[test]
fn test_client_creation() {
let client = FlexApiClient::new("my_token");
assert_eq!(client.token, "my_token");
assert_eq!(client.base_url, FLEX_BASE_URL);
}
#[test]
fn test_client_with_custom_url() {
let client = FlexApiClient::with_base_url("my_token", "https://custom.url");
assert_eq!(client.token, "my_token");
assert_eq!(client.base_url, "https://custom.url");
}
}