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/// Active state - session is fully established.
47#[derive(Debug, Clone, Copy)]
48pub struct Active;
49
50impl private::Sealed for Active {}
51impl SessionState for Active {}
52
53/// Resending state - processing a resend request.
54#[derive(Debug, Clone)]
55pub struct Resending {
56    /// Begin sequence number of the gap.
57    pub begin_seq: u64,
58    /// End sequence number of the gap.
59    pub end_seq: u64,
60}
61
62impl private::Sealed for Resending {}
63impl SessionState for Resending {}
64
65/// LogoutPending state - Logout sent, awaiting confirmation.
66#[derive(Debug, Clone)]
67pub struct LogoutPending {
68    /// Time when Logout was sent.
69    pub sent_at: Instant,
70}
71
72impl private::Sealed for LogoutPending {}
73impl SessionState for LogoutPending {}
74
75/// Session wrapper with typestate for compile-time state checking.
76///
77/// The type parameter `S` represents the current session state.
78#[derive(Debug)]
79pub struct Session<S: SessionState> {
80    /// Session identifier.
81    pub session_id: String,
82    /// Phantom data for the state type.
83    _state: PhantomData<S>,
84}
85
86impl<S: SessionState> Session<S> {
87    /// Returns the session identifier.
88    #[must_use]
89    pub fn session_id(&self) -> &str {
90        &self.session_id
91    }
92}
93
94impl Session<Disconnected> {
95    /// Creates a new disconnected session.
96    ///
97    /// # Arguments
98    /// * `session_id` - Unique identifier for this session
99    #[must_use]
100    pub fn new(session_id: impl Into<String>) -> Self {
101        Self {
102            session_id: session_id.into(),
103            _state: PhantomData,
104        }
105    }
106
107    /// Transitions to the Connecting state.
108    #[must_use]
109    pub fn connect(self) -> Session<Connecting> {
110        Session {
111            session_id: self.session_id,
112            _state: PhantomData,
113        }
114    }
115}
116
117impl Session<Connecting> {
118    /// Transitions to the LogonSent state after sending Logon.
119    #[must_use]
120    pub fn send_logon(self) -> Session<LogonSent> {
121        Session {
122            session_id: self.session_id,
123            _state: PhantomData,
124        }
125    }
126
127    /// Transitions back to Disconnected on connection failure.
128    #[must_use]
129    pub fn disconnect(self) -> Session<Disconnected> {
130        Session {
131            session_id: self.session_id,
132            _state: PhantomData,
133        }
134    }
135}
136
137impl Session<LogonSent> {
138    /// Transitions to Active state on successful Logon acknowledgement.
139    #[must_use]
140    pub fn on_logon_ack(self) -> Session<Active> {
141        Session {
142            session_id: self.session_id,
143            _state: PhantomData,
144        }
145    }
146
147    /// Transitions to Disconnected on Logon rejection or timeout.
148    #[must_use]
149    pub fn on_logon_reject(self) -> Session<Disconnected> {
150        Session {
151            session_id: self.session_id,
152            _state: PhantomData,
153        }
154    }
155}
156
157impl Session<Active> {
158    /// Transitions to Resending state when a gap is detected.
159    ///
160    /// # Arguments
161    /// * `begin_seq` - Begin sequence number of the gap
162    /// * `end_seq` - End sequence number of the gap
163    #[must_use]
164    pub fn start_resend(self, _begin_seq: u64, _end_seq: u64) -> Session<Resending> {
165        Session {
166            session_id: self.session_id,
167            _state: PhantomData,
168        }
169    }
170
171    /// Transitions to LogoutPending state.
172    #[must_use]
173    pub fn initiate_logout(self) -> Session<LogoutPending> {
174        Session {
175            session_id: self.session_id,
176            _state: PhantomData,
177        }
178    }
179
180    /// Transitions to Disconnected on unexpected disconnect.
181    #[must_use]
182    pub fn disconnect(self) -> Session<Disconnected> {
183        Session {
184            session_id: self.session_id,
185            _state: PhantomData,
186        }
187    }
188}
189
190impl Session<Resending> {
191    /// Transitions back to Active when resend is complete.
192    #[must_use]
193    pub fn resend_complete(self) -> Session<Active> {
194        Session {
195            session_id: self.session_id,
196            _state: PhantomData,
197        }
198    }
199
200    /// Transitions to Disconnected on error.
201    #[must_use]
202    pub fn disconnect(self) -> Session<Disconnected> {
203        Session {
204            session_id: self.session_id,
205            _state: PhantomData,
206        }
207    }
208}
209
210impl Session<LogoutPending> {
211    /// Transitions to Disconnected on Logout acknowledgement or timeout.
212    #[must_use]
213    pub fn on_logout_ack(self) -> Session<Disconnected> {
214        Session {
215            session_id: self.session_id,
216            _state: PhantomData,
217        }
218    }
219
220    /// Transitions to Disconnected on timeout.
221    #[must_use]
222    pub fn on_timeout(self) -> Session<Disconnected> {
223        Session {
224            session_id: self.session_id,
225            _state: PhantomData,
226        }
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_session_state_transitions() {
236        let session = Session::<Disconnected>::new("TEST");
237        assert_eq!(session.session_id(), "TEST");
238
239        let session = session.connect();
240        let session = session.send_logon();
241        let session = session.on_logon_ack();
242
243        // Now in Active state
244        let session = session.initiate_logout();
245        let _session = session.on_logout_ack();
246    }
247
248    #[test]
249    fn test_resend_flow() {
250        let session = Session::<Disconnected>::new("TEST");
251        let session = session.connect();
252        let session = session.send_logon();
253        let session = session.on_logon_ack();
254
255        let session = session.start_resend(1, 5);
256        let _session = session.resend_complete();
257    }
258}