Skip to main content

ironfix_session/
state.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 27/1/26
5******************************************************************************/
6
7//! Session state machine using the typestate pattern.
8//!
9//! This module implements a compile-time checked state machine for FIX sessions.
10//! State transitions are enforced by the type system, preventing invalid operations.
11
12use std::marker::PhantomData;
13use std::time::Instant;
14
15/// Marker trait for session states.
16pub trait SessionState: private::Sealed {}
17
18mod private {
19    pub trait Sealed {}
20}
21
22/// Disconnected state - no connection established.
23#[derive(Debug, Clone, Copy)]
24pub struct Disconnected;
25
26impl private::Sealed for Disconnected {}
27impl SessionState for Disconnected {}
28
29/// Connecting state - TCP connection in progress.
30#[derive(Debug, Clone, Copy)]
31pub struct Connecting;
32
33impl private::Sealed for Connecting {}
34impl SessionState for Connecting {}
35
36/// LogonSent state - Logon message sent, awaiting response.
37#[derive(Debug, Clone)]
38pub struct LogonSent {
39    /// Time when Logon was sent.
40    pub sent_at: Instant,
41}
42
43impl private::Sealed for LogonSent {}
44impl SessionState for LogonSent {}
45
46/// LogonReceived state - Logon received from counterparty (acceptor side),
47/// pending authentication.
48#[derive(Debug, Clone)]
49pub struct LogonReceived {
50    /// Time when the Logon was received.
51    pub received_at: Instant,
52}
53
54impl private::Sealed for LogonReceived {}
55impl SessionState for LogonReceived {}
56
57/// Active state - session is fully established.
58#[derive(Debug, Clone, Copy)]
59pub struct Active;
60
61impl private::Sealed for Active {}
62impl SessionState for Active {}
63
64/// Resending state - processing a resend request.
65#[derive(Debug, Clone)]
66pub struct Resending {
67    /// Begin sequence number of the gap.
68    pub begin_seq: u64,
69    /// End sequence number of the gap.
70    pub end_seq: u64,
71}
72
73impl private::Sealed for Resending {}
74impl SessionState for Resending {}
75
76/// LogoutPending state - Logout sent, awaiting confirmation.
77#[derive(Debug, Clone)]
78pub struct LogoutPending {
79    /// Time when Logout was sent.
80    pub sent_at: Instant,
81}
82
83impl private::Sealed for LogoutPending {}
84impl SessionState for LogoutPending {}
85
86/// Session wrapper with typestate for compile-time state checking.
87///
88/// The type parameter `S` represents the current session state.
89#[derive(Debug)]
90pub struct Session<S: SessionState> {
91    /// Session identifier.
92    pub session_id: String,
93    /// Phantom data for the state type.
94    _state: PhantomData<S>,
95}
96
97impl<S: SessionState> Session<S> {
98    /// Returns the session identifier.
99    #[must_use]
100    pub fn session_id(&self) -> &str {
101        &self.session_id
102    }
103}
104
105impl Session<Disconnected> {
106    /// Creates a new disconnected session.
107    ///
108    /// # Arguments
109    /// * `session_id` - Unique identifier for this session
110    #[must_use]
111    pub fn new(session_id: impl Into<String>) -> Self {
112        Self {
113            session_id: session_id.into(),
114            _state: PhantomData,
115        }
116    }
117
118    /// Transitions to the Connecting state (initiator side).
119    #[must_use]
120    pub fn connect(self) -> Session<Connecting> {
121        Session {
122            session_id: self.session_id,
123            _state: PhantomData,
124        }
125    }
126
127    /// Transitions to the Connecting state after accepting an inbound
128    /// TCP connection (acceptor side).
129    #[must_use]
130    pub fn accept(self) -> Session<Connecting> {
131        Session {
132            session_id: self.session_id,
133            _state: PhantomData,
134        }
135    }
136}
137
138impl Session<Connecting> {
139    /// Transitions to the LogonSent state after sending Logon (initiator side).
140    #[must_use]
141    pub fn send_logon(self) -> Session<LogonSent> {
142        Session {
143            session_id: self.session_id,
144            _state: PhantomData,
145        }
146    }
147
148    /// Transitions to the LogonReceived state when a Logon arrives from
149    /// the counterparty (acceptor side).
150    #[must_use]
151    pub fn on_logon_received(self) -> Session<LogonReceived> {
152        Session {
153            session_id: self.session_id,
154            _state: PhantomData,
155        }
156    }
157
158    /// Transitions back to Disconnected on connection failure.
159    #[must_use]
160    pub fn disconnect(self) -> Session<Disconnected> {
161        Session {
162            session_id: self.session_id,
163            _state: PhantomData,
164        }
165    }
166}
167
168impl Session<LogonSent> {
169    /// Transitions to Active state on successful Logon acknowledgement.
170    #[must_use]
171    pub fn on_logon_ack(self) -> Session<Active> {
172        Session {
173            session_id: self.session_id,
174            _state: PhantomData,
175        }
176    }
177
178    /// Transitions to Disconnected on Logon rejection or timeout.
179    #[must_use]
180    pub fn on_logon_reject(self) -> Session<Disconnected> {
181        Session {
182            session_id: self.session_id,
183            _state: PhantomData,
184        }
185    }
186}
187
188impl Session<LogonReceived> {
189    /// Transitions to Active after successful authentication, once the
190    /// Logon acknowledgement has been sent back to the counterparty.
191    #[must_use]
192    pub fn accept_logon(self) -> Session<Active> {
193        Session {
194            session_id: self.session_id,
195            _state: PhantomData,
196        }
197    }
198
199    /// Transitions to Disconnected when authentication fails and the
200    /// Logon is rejected (Logout/Reject sent, connection dropped).
201    #[must_use]
202    pub fn reject_logon(self) -> Session<Disconnected> {
203        Session {
204            session_id: self.session_id,
205            _state: PhantomData,
206        }
207    }
208
209    /// Transitions to Disconnected when authentication does not complete
210    /// within the allowed time.
211    #[must_use]
212    pub fn on_timeout(self) -> Session<Disconnected> {
213        Session {
214            session_id: self.session_id,
215            _state: PhantomData,
216        }
217    }
218}
219
220impl Session<Active> {
221    /// Transitions to Resending state when a gap is detected.
222    ///
223    /// # Arguments
224    /// * `begin_seq` - Begin sequence number of the gap
225    /// * `end_seq` - End sequence number of the gap
226    #[must_use]
227    pub fn start_resend(self, _begin_seq: u64, _end_seq: u64) -> Session<Resending> {
228        Session {
229            session_id: self.session_id,
230            _state: PhantomData,
231        }
232    }
233
234    /// Transitions to LogoutPending state.
235    #[must_use]
236    pub fn initiate_logout(self) -> Session<LogoutPending> {
237        Session {
238            session_id: self.session_id,
239            _state: PhantomData,
240        }
241    }
242
243    /// Transitions to Disconnected on unexpected disconnect.
244    #[must_use]
245    pub fn disconnect(self) -> Session<Disconnected> {
246        Session {
247            session_id: self.session_id,
248            _state: PhantomData,
249        }
250    }
251}
252
253impl Session<Resending> {
254    /// Transitions back to Active when resend is complete.
255    #[must_use]
256    pub fn resend_complete(self) -> Session<Active> {
257        Session {
258            session_id: self.session_id,
259            _state: PhantomData,
260        }
261    }
262
263    /// Transitions to Disconnected on error.
264    #[must_use]
265    pub fn disconnect(self) -> Session<Disconnected> {
266        Session {
267            session_id: self.session_id,
268            _state: PhantomData,
269        }
270    }
271}
272
273impl Session<LogoutPending> {
274    /// Transitions to Disconnected on Logout acknowledgement or timeout.
275    #[must_use]
276    pub fn on_logout_ack(self) -> Session<Disconnected> {
277        Session {
278            session_id: self.session_id,
279            _state: PhantomData,
280        }
281    }
282
283    /// Transitions to Disconnected on timeout.
284    #[must_use]
285    pub fn on_timeout(self) -> Session<Disconnected> {
286        Session {
287            session_id: self.session_id,
288            _state: PhantomData,
289        }
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_session_state_transitions() {
299        let session = Session::<Disconnected>::new("TEST");
300        assert_eq!(session.session_id(), "TEST");
301
302        let session = session.connect();
303        let session = session.send_logon();
304        let session = session.on_logon_ack();
305
306        // Now in Active state
307        let session = session.initiate_logout();
308        let _session = session.on_logout_ack();
309    }
310
311    #[test]
312    fn test_acceptor_flow() {
313        let session = Session::<Disconnected>::new("ACCEPTOR");
314        let session = session.accept();
315        let session = session.on_logon_received();
316        let session = session.accept_logon();
317
318        // Now in Active state
319        let session = session.initiate_logout();
320        let _session = session.on_logout_ack();
321    }
322
323    #[test]
324    fn test_acceptor_reject_flow() {
325        let session = Session::<Disconnected>::new("ACCEPTOR");
326        let session = session.accept();
327        let session = session.on_logon_received();
328        let _session = session.reject_logon();
329    }
330
331    #[test]
332    fn test_acceptor_timeout_flow() {
333        let session = Session::<Disconnected>::new("ACCEPTOR");
334        let session = session.accept();
335        let session = session.on_logon_received();
336        let _session = session.on_timeout();
337    }
338
339    #[test]
340    fn test_resend_flow() {
341        let session = Session::<Disconnected>::new("TEST");
342        let session = session.connect();
343        let session = session.send_logon();
344        let session = session.on_logon_ack();
345
346        let session = session.start_resend(1, 5);
347        let _session = session.resend_complete();
348    }
349}