use reqwest::header::HeaderMap;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fs;
use std::str::FromStr;
use std::string::ToString;
#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum ExecutionEnvironment {
Demo,
Live,
}
impl Default for ExecutionEnvironment {
fn default() -> Self {
ExecutionEnvironment::Demo
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub enum LogType
{
StdLogs,
TracingLogs,
}
impl<'de> Deserialize<'de> for ExecutionEnvironment {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?.to_uppercase();
match s.as_str() {
"DEMO" => Ok(ExecutionEnvironment::Demo),
"LIVE" => Ok(ExecutionEnvironment::Live),
_ => Err(serde::de::Error::custom("Invalid account type")),
}
}
}
impl FromStr for ExecutionEnvironment {
type Err = ApiError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_uppercase().as_str() {
"DEMO" => Ok(ExecutionEnvironment::Demo),
"LIVE" => Ok(ExecutionEnvironment::Live),
_ => Err(ApiError {
message: format!("Invalid account type: {}", s),
}),
}
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
pub struct ApiConfig {
#[serde(skip_deserializing)]
pub account_number_demo: String,
#[serde(skip_deserializing)]
pub account_number_live: String,
#[serde(skip_deserializing)]
pub account_number_test: Option<String>,
#[serde(skip_deserializing)]
pub api_key: String,
pub auto_login: Option<bool>,
#[serde(skip_deserializing)]
pub base_url_demo: String,
#[serde(skip_deserializing)]
pub base_url_live: String,
#[serde(skip_deserializing)]
pub execution_environment: ExecutionEnvironment,
pub logger: LogType,
#[serde(skip_deserializing)]
pub password: String,
pub session_version: Option<usize>,
pub streaming_api_max_connection_attempts: Option<u64>,
#[serde(skip_deserializing)]
pub username: String,
}
impl ApiConfig {
pub fn new() -> Self {
ApiConfig {
account_number_demo: "".to_string(),
account_number_live: "".to_string(),
account_number_test: None,
api_key: "".to_string(),
auto_login: None,
base_url_demo: "".to_string(),
base_url_live: "".to_string(),
execution_environment: ExecutionEnvironment::Demo,
logger: LogType::StdLogs,
password: "".to_string(),
session_version: None,
streaming_api_max_connection_attempts: None,
username: "".to_string(),
}
}
pub fn load_env() -> Result<(), Box<dyn Error>> {
match dotenvy::dotenv() {
Ok(_) => (),
Err(e) => {
eprintln!("Warning: .env file not found or couldn't be loaded: {}", e);
}
}
Ok(())
}
fn get_required_env(key: &str) -> String {
env::var(key).unwrap_or_else(|_| {
panic!(
"Environment variable {} is required but not set. Please check your .env file.",
key
)
})
}
fn get_optional_env(key: &str) -> Option<String> {
env::var(key).ok()
}
pub fn from_env_and_config() -> Result<Self, Box<dyn Error>> {
Self::load_env()?;
let mut config = if std::path::Path::new("config.yaml").exists() {
let config_contents = fs::read_to_string("config.yaml")?;
let yaml_config: HashMap<String, serde_yaml::Value> =
serde_yaml::from_str(&config_contents)?;
if let Some(api_config_value) = yaml_config.get("ig_trading_api") {
serde_yaml::from_value::<ApiConfig>(api_config_value.clone())?
} else {
ApiConfig::new()
}
} else {
eprintln!("Warning: config.yaml not found. Using default values for application settings.");
ApiConfig::new()
};
config.api_key = Self::get_required_env("IG_API_KEY");
config.username = Self::get_required_env("IG_USERNAME");
config.password = Self::get_required_env("IG_PASSWORD");
config.account_number_demo = Self::get_required_env("IG_ACCOUNT_NUMBER_DEMO");
config.account_number_live = Self::get_required_env("IG_ACCOUNT_NUMBER_LIVE");
config.account_number_test = Self::get_optional_env("IG_ACCOUNT_NUMBER_TEST");
config.base_url_demo = Self::get_required_env("IG_BASE_URL_DEMO");
config.base_url_live = Self::get_required_env("IG_BASE_URL_LIVE");
let env_str = Self::get_required_env("IG_EXECUTION_ENVIRONMENT");
config.execution_environment = ExecutionEnvironment::from_str(&env_str)?;
Ok(config)
}
}
impl Default for ApiConfig {
fn default() -> Self {
Self::from_env_and_config().unwrap_or_else(|e| {
panic!("Failed to load API configuration: {}", e);
})
}
}
#[derive(Debug)]
pub struct ApiError {
pub message: String,
}
impl std::fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "API error: {}", self.message)
}
}
impl std::error::Error for ApiError {}
pub fn params_to_query_string<T: Serialize>(
data: &T,
) -> Result<String, serde_urlencoded::ser::Error> {
serde_urlencoded::to_string(data)
}
pub fn headers_to_json(headers: &HeaderMap) -> Result<Value, Box<dyn Error>> {
let mut map = serde_json::Map::new();
for (key, value) in headers {
map.insert(
key.as_str().to_string(),
Value::String(value.to_str()?.to_string()),
);
}
Ok(Value::Object(map))
}