ig_client/session/interface.rs
1use crate::config::Config;
2use crate::error::{AppError, AuthError};
3use crate::utils::rate_limiter::{
4 RateLimitType, RateLimiter, RateLimiterStats, app_non_trading_limiter, create_rate_limiter,
5};
6use std::sync::Arc;
7use std::sync::atomic::{AtomicBool, Ordering};
8use tracing::debug;
9
10/// Session information for IG Markets API authentication
11#[derive(Debug, Clone)]
12pub struct IgSession {
13 /// Client Session Token (CST) used for authentication
14 pub cst: String,
15 /// Security token used for authentication
16 pub token: String,
17 /// Account ID associated with the session
18 pub account_id: String,
19 /// Base URL for API requests
20 pub base_url: String,
21 /// Client ID for API requests
22 pub client_id: String,
23 /// Lightstreamer endpoint for API requests
24 pub lightstreamer_endpoint: String,
25 /// API key for API requests
26 pub api_key: String,
27 /// Rate limiter for controlling request rates
28 pub(crate) rate_limiter: Option<Arc<RateLimiter>>,
29 /// Flag to indicate if the session is being used in a concurrent context
30 pub(crate) concurrent_mode: Arc<AtomicBool>,
31}
32
33impl IgSession {
34 /// Creates a new session with the given credentials
35 ///
36 /// This is a simplified version for tests and basic usage.
37 /// Uses default values for most fields and a default rate limiter.
38 pub fn new(cst: String, token: String, account_id: String) -> Self {
39 Self {
40 base_url: String::new(),
41 cst,
42 token,
43 client_id: String::new(),
44 account_id,
45 lightstreamer_endpoint: String::new(),
46 api_key: String::new(),
47 rate_limiter: Some(create_rate_limiter(
48 RateLimitType::NonTradingAccount,
49 Some(0.8),
50 )),
51 concurrent_mode: Arc::new(AtomicBool::new(false)),
52 }
53 }
54
55 /// Creates a new session with the given parameters
56 ///
57 /// This creates a thread-safe session that can be shared across multiple threads.
58 /// The rate limiter is wrapped in an Arc to ensure proper synchronization.
59 #[allow(clippy::too_many_arguments)]
60 pub fn new_with_config(
61 base_url: String,
62 cst: String,
63 security_token: String,
64 client_id: String,
65 account_id: String,
66 lightstreamer_endpoint: String,
67 api_key: String,
68 rate_limit_type: RateLimitType,
69 rate_limit_safety_margin: f64,
70 ) -> Self {
71 // Create a rate limiter with the specified type and safety margin
72 let rate_limiter = create_rate_limiter(rate_limit_type, Some(rate_limit_safety_margin));
73
74 Self {
75 base_url,
76 cst,
77 token: security_token,
78 client_id,
79 account_id,
80 lightstreamer_endpoint,
81 api_key,
82 rate_limiter: Some(rate_limiter),
83 concurrent_mode: Arc::new(AtomicBool::new(false)),
84 }
85 }
86
87 /// Creates a new session with the given credentials and a rate limiter
88 ///
89 /// This creates a thread-safe session that can be shared across multiple threads.
90 pub fn with_rate_limiter(
91 cst: String,
92 token: String,
93 account_id: String,
94 limit_type: RateLimitType,
95 ) -> Self {
96 Self {
97 cst,
98 token,
99 account_id,
100 base_url: String::new(),
101 client_id: String::new(),
102 lightstreamer_endpoint: String::new(),
103 api_key: String::new(),
104 rate_limiter: Some(create_rate_limiter(limit_type, Some(0.8))),
105 concurrent_mode: Arc::new(AtomicBool::new(false)),
106 }
107 }
108
109 /// Creates a new session with the given credentials and rate limiter configuration from Config
110 pub fn from_config(cst: String, token: String, account_id: String, config: &Config) -> Self {
111 Self {
112 cst,
113 token,
114 account_id,
115 base_url: String::new(),
116 client_id: String::new(),
117 lightstreamer_endpoint: String::new(),
118 api_key: String::new(),
119 rate_limiter: Some(create_rate_limiter(
120 config.rate_limit_type,
121 Some(config.rate_limit_safety_margin),
122 )),
123 concurrent_mode: Arc::new(AtomicBool::new(false)),
124 }
125 }
126
127 /// Waits if necessary to respect rate limits before making a request
128 ///
129 /// This method will always use a rate limiter - either the one configured in the session,
130 /// or a default one if none is configured.
131 ///
132 /// This method is thread-safe and can be called from multiple threads concurrently.
133 ///
134 /// # Returns
135 /// * `Ok(())` - If the rate limit is respected
136 /// * `Err(AppError::RateLimitExceeded)` - If the rate limit has been exceeded and cannot be respected
137 pub async fn respect_rate_limit(&self) -> Result<(), AppError> {
138 // Mark that this session is being used in a concurrent context
139 self.concurrent_mode.store(true, Ordering::SeqCst);
140
141 // Get the rate limiter from the session or use a default one
142 let limiter = match &self.rate_limiter {
143 Some(limiter) => limiter.clone(),
144 None => {
145 // This should never happen since we always initialize with a default limiter,
146 // but just in case, use the global app non-trading limiter
147 debug!("No rate limiter configured in session, using default");
148 app_non_trading_limiter()
149 }
150 };
151
152 // Wait if necessary to respect the rate limit
153 limiter.wait().await;
154 Ok(())
155 }
156
157 /// Gets statistics about the current rate limit usage
158 pub async fn get_rate_limit_stats(&self) -> Option<RateLimiterStats> {
159 match &self.rate_limiter {
160 Some(limiter) => Some(limiter.get_stats().await),
161 None => None,
162 }
163 }
164}
165
166/// Trait for authenticating with the IG Markets API
167#[async_trait::async_trait]
168pub trait IgAuthenticator: Send + Sync {
169 /// Logs in to the IG Markets API and returns a new session
170 async fn login(&self) -> Result<IgSession, AuthError>;
171 /// Refreshes an existing session with the IG Markets API
172 async fn refresh(&self, session: &IgSession) -> Result<IgSession, AuthError>;
173 /// Switches the active account for the current session
174 ///
175 /// # Arguments
176 /// * `session` - The current session
177 /// * `account_id` - The ID of the account to switch to
178 /// * `default_account` - Whether to set this account as the default (optional)
179 ///
180 /// # Returns
181 /// * A new session with the updated account ID
182 async fn switch_account(
183 &self,
184 session: &IgSession,
185 account_id: &str,
186 default_account: Option<bool>,
187 ) -> Result<IgSession, AuthError>;
188}