Skip to main content

hotfix/
message.rs

1//! FIX message abstractions to help with encoding and parsing of messages.
2use hotfix_message::error::EncodingError as EncodeError;
3pub use hotfix_message::field_types::Timestamp;
4pub(crate) use hotfix_message::message::{Config, Message};
5use hotfix_message::session_fields::{
6    MSG_SEQ_NUM, ORIG_SENDING_TIME, POSS_DUP_FLAG, SENDER_COMP_ID, SENDING_TIME, TARGET_COMP_ID,
7};
8pub use hotfix_message::{Part, RepeatingGroup};
9
10pub mod business_reject;
11pub mod heartbeat;
12pub mod logon;
13pub mod logout;
14pub mod parser;
15pub mod reject;
16pub mod resend_request;
17pub mod sequence_reset;
18pub mod test_request;
19pub mod verification;
20pub mod verification_issue;
21
22pub use parser::RawFixMessage;
23pub use resend_request::ResendRequest;
24
25use heartbeat::Heartbeat;
26use logon::Logon;
27use logout::Logout;
28use reject::Reject;
29use sequence_reset::SequenceReset;
30use test_request::TestRequest;
31
32static ADMIN_TYPES: [&str; 7] = [
33    Logon::MSG_TYPE,
34    Heartbeat::MSG_TYPE,
35    TestRequest::MSG_TYPE,
36    ResendRequest::MSG_TYPE,
37    Reject::MSG_TYPE,
38    SequenceReset::MSG_TYPE,
39    Logout::MSG_TYPE,
40];
41
42pub fn is_admin(message_type: &str) -> bool {
43    ADMIN_TYPES.contains(&message_type)
44}
45
46pub trait OutboundMessage: Clone + Send + 'static {
47    fn write(&self, msg: &mut Message);
48
49    fn message_type(&self) -> &str;
50}
51
52/// Prepares a FIX message for resend per the FIX spec (PossDupFlag logic).
53///
54/// Behaviour:
55/// - On first resend (no PossDupFlag Y / no OrigSendingTime):
56///   * Move current SendingTime(52) to OrigSendingTime(122)
57///   * Set SendingTime(52) to the current sending time (may be equal if clock granularity causes no change)
58///   * Set PossDupFlag(43)=Y
59/// - On subsequent resends (already marked possible duplicate and has OrigSendingTime):
60///   * Refresh SendingTime(52) to current time (value may or may not differ from previous)
61pub fn prepare_message_for_resend(msg: &mut Message) -> Result<(), &'static str> {
62    let header = msg.header_mut();
63
64    if header.get_raw(SENDING_TIME).is_none() {
65        return Err("Missing SendingTime (52)");
66    }
67
68    let already_poss_dup = header.get::<bool>(POSS_DUP_FLAG).unwrap_or(false);
69    let has_orig_sending_time = header.get_raw(ORIG_SENDING_TIME).is_some();
70
71    if already_poss_dup && has_orig_sending_time {
72        // Subsequent resend: refresh SendingTime only
73        return if header.pop(SENDING_TIME).is_some() {
74            header.set(SENDING_TIME, Timestamp::utc_now());
75            Ok(())
76        } else {
77            Err("Failed to extract previous SendingTime")
78        };
79    }
80
81    // First resend path
82    if let Some(original_sending_time_field) = header.pop(SENDING_TIME) {
83        let original_ts = Timestamp::parse(&original_sending_time_field.data)
84            .ok_or("Invalid original SendingTime format")?;
85        header.set(ORIG_SENDING_TIME, original_ts);
86        header.set(SENDING_TIME, Timestamp::utc_now());
87        header.set(POSS_DUP_FLAG, true);
88        Ok(())
89    } else {
90        Err("Failed to extract original SendingTime")
91    }
92}
93
94pub fn generate_message(
95    begin_string: &str,
96    sender_comp_id: &str,
97    target_comp_id: &str,
98    msg_seq_num: u64,
99    message: impl OutboundMessage,
100) -> Result<Vec<u8>, EncodeError> {
101    let mut msg = Message::new(begin_string, message.message_type());
102    msg.set(SENDER_COMP_ID, sender_comp_id);
103    msg.set(TARGET_COMP_ID, target_comp_id.as_bytes());
104    msg.set(MSG_SEQ_NUM, msg_seq_num);
105    msg.set(SENDING_TIME, Timestamp::utc_now());
106
107    message.write(&mut msg);
108
109    msg.encode(&Config::default())
110}