Skip to main content

ironfix_engine/
application.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 27/1/26
5******************************************************************************/
6
7//! Application callback interface.
8//!
9//! This module defines the callback interface for handling FIX messages,
10//! following the QuickFIX pattern with async support.
11
12use async_trait::async_trait;
13use ironfix_core::message::{OwnedMessage, RawMessage};
14
15/// Session identifier.
16#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct SessionId {
18    /// BeginString (FIX version).
19    pub begin_string: String,
20    /// Sender CompID.
21    pub sender_comp_id: String,
22    /// Target CompID.
23    pub target_comp_id: String,
24    /// Optional sender sub ID.
25    pub sender_sub_id: Option<String>,
26    /// Optional target sub ID.
27    pub target_sub_id: Option<String>,
28}
29
30impl SessionId {
31    /// Creates a new session ID.
32    #[must_use]
33    pub fn new(
34        begin_string: impl Into<String>,
35        sender_comp_id: impl Into<String>,
36        target_comp_id: impl Into<String>,
37    ) -> Self {
38        Self {
39            begin_string: begin_string.into(),
40            sender_comp_id: sender_comp_id.into(),
41            target_comp_id: target_comp_id.into(),
42            sender_sub_id: None,
43            target_sub_id: None,
44        }
45    }
46
47    /// Sets the sender sub ID.
48    #[must_use]
49    pub fn with_sender_sub_id(mut self, sub_id: impl Into<String>) -> Self {
50        self.sender_sub_id = Some(sub_id.into());
51        self
52    }
53
54    /// Sets the target sub ID.
55    #[must_use]
56    pub fn with_target_sub_id(mut self, sub_id: impl Into<String>) -> Self {
57        self.target_sub_id = Some(sub_id.into());
58        self
59    }
60}
61
62impl std::fmt::Display for SessionId {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        write!(
65            f,
66            "{}:{}->{}",
67            self.begin_string, self.sender_comp_id, self.target_comp_id
68        )
69    }
70}
71
72/// Reason for rejecting a message.
73#[derive(Debug, Clone)]
74pub struct RejectReason {
75    /// Rejection reason code.
76    pub code: u32,
77    /// Human-readable rejection text.
78    pub text: String,
79    /// Reference tag that caused the rejection.
80    pub ref_tag: Option<u32>,
81}
82
83impl RejectReason {
84    /// Creates a new rejection reason.
85    #[must_use]
86    pub fn new(code: u32, text: impl Into<String>) -> Self {
87        Self {
88            code,
89            text: text.into(),
90            ref_tag: None,
91        }
92    }
93
94    /// Sets the reference tag.
95    #[must_use]
96    pub const fn with_ref_tag(mut self, tag: u32) -> Self {
97        self.ref_tag = Some(tag);
98        self
99    }
100}
101
102/// Application callback interface for handling FIX messages.
103///
104/// Implement this trait to receive callbacks for session events
105/// and message processing.
106#[async_trait]
107pub trait Application: Send + Sync {
108    /// Called when a session is created.
109    ///
110    /// # Arguments
111    /// * `session_id` - The session identifier
112    async fn on_create(&self, session_id: &SessionId);
113
114    /// Called on successful logon.
115    ///
116    /// # Arguments
117    /// * `session_id` - The session identifier
118    async fn on_logon(&self, session_id: &SessionId);
119
120    /// Called on logout.
121    ///
122    /// # Arguments
123    /// * `session_id` - The session identifier
124    async fn on_logout(&self, session_id: &SessionId);
125
126    /// Called before sending an admin message.
127    ///
128    /// Allows modification of outgoing admin messages (Logon, Heartbeat, etc.).
129    ///
130    /// # Arguments
131    /// * `message` - The message to be sent (mutable)
132    /// * `session_id` - The session identifier
133    async fn to_admin(&self, message: &mut OwnedMessage, session_id: &SessionId);
134
135    /// Called when an admin message is received.
136    ///
137    /// # Arguments
138    /// * `message` - The received message
139    /// * `session_id` - The session identifier
140    ///
141    /// # Returns
142    /// `Ok(())` to accept, `Err(RejectReason)` to reject.
143    #[allow(clippy::wrong_self_convention)]
144    async fn from_admin(
145        &self,
146        message: &RawMessage<'_>,
147        session_id: &SessionId,
148    ) -> Result<(), RejectReason>;
149
150    /// Called before sending an application message.
151    ///
152    /// Allows modification of outgoing application messages.
153    ///
154    /// # Arguments
155    /// * `message` - The message to be sent (mutable)
156    /// * `session_id` - The session identifier
157    async fn to_app(&self, message: &mut OwnedMessage, session_id: &SessionId);
158
159    /// Called when an application message is received.
160    ///
161    /// # Arguments
162    /// * `message` - The received message
163    /// * `session_id` - The session identifier
164    ///
165    /// # Returns
166    /// `Ok(())` to accept, `Err(RejectReason)` to reject.
167    #[allow(clippy::wrong_self_convention)]
168    async fn from_app(
169        &self,
170        message: &RawMessage<'_>,
171        session_id: &SessionId,
172    ) -> Result<(), RejectReason>;
173}
174
175/// Default no-op application implementation.
176#[derive(Debug, Default)]
177pub struct NoOpApplication;
178
179#[async_trait]
180impl Application for NoOpApplication {
181    async fn on_create(&self, _session_id: &SessionId) {}
182
183    async fn on_logon(&self, _session_id: &SessionId) {}
184
185    async fn on_logout(&self, _session_id: &SessionId) {}
186
187    async fn to_admin(&self, _message: &mut OwnedMessage, _session_id: &SessionId) {}
188
189    async fn from_admin(
190        &self,
191        _message: &RawMessage<'_>,
192        _session_id: &SessionId,
193    ) -> Result<(), RejectReason> {
194        Ok(())
195    }
196
197    async fn to_app(&self, _message: &mut OwnedMessage, _session_id: &SessionId) {}
198
199    async fn from_app(
200        &self,
201        _message: &RawMessage<'_>,
202        _session_id: &SessionId,
203    ) -> Result<(), RejectReason> {
204        Ok(())
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn test_session_id() {
214        let id = SessionId::new("FIX.4.4", "SENDER", "TARGET");
215        assert_eq!(id.begin_string, "FIX.4.4");
216        assert_eq!(id.sender_comp_id, "SENDER");
217        assert_eq!(id.target_comp_id, "TARGET");
218        assert_eq!(id.to_string(), "FIX.4.4:SENDER->TARGET");
219    }
220
221    #[test]
222    fn test_reject_reason() {
223        let reason = RejectReason::new(1, "Invalid tag").with_ref_tag(35);
224        assert_eq!(reason.code, 1);
225        assert_eq!(reason.text, "Invalid tag");
226        assert_eq!(reason.ref_tag, Some(35));
227    }
228
229    #[tokio::test]
230    async fn test_noop_application() {
231        let app = NoOpApplication;
232        let session_id = SessionId::new("FIX.4.4", "SENDER", "TARGET");
233
234        app.on_create(&session_id).await;
235        app.on_logon(&session_id).await;
236        app.on_logout(&session_id).await;
237    }
238}