hotfix 0.12.0

Buy-side FIX engine written in pure Rust
Documentation
use crate::message::OutboundMessage;
use hotfix_message::Part;
use hotfix_message::message::Message;
use hotfix_message::session_fields::{
    MsgType, REF_MSG_TYPE, REF_SEQ_NUM, REF_TAG_ID, SESSION_REJECT_REASON, SessionRejectReason,
    TEXT,
};

#[derive(Clone, Debug)]
pub(crate) struct Reject {
    ref_seq_num: u64,
    ref_tag_id: Option<u64>,
    ref_msg_type: Option<MsgType>,
    session_reject_reason: Option<SessionRejectReason>,
    text: Option<String>,
}

impl Reject {
    pub(crate) const MSG_TYPE: &str = "3";

    pub(crate) fn new(ref_seq_num: u64) -> Self {
        Self {
            ref_seq_num,
            ref_tag_id: None,
            ref_msg_type: None,
            session_reject_reason: None,
            text: None,
        }
    }

    #[allow(dead_code)]
    pub(crate) fn ref_tag_id(mut self, ref_tag_id: u64) -> Self {
        self.ref_tag_id = Some(ref_tag_id);
        self
    }

    #[allow(dead_code)]
    pub(crate) fn ref_msg_type(mut self, ref_msg_type: MsgType) -> Self {
        self.ref_msg_type = Some(ref_msg_type);
        self
    }

    pub(crate) fn session_reject_reason(
        mut self,
        session_reject_reason: SessionRejectReason,
    ) -> Self {
        self.session_reject_reason = Some(session_reject_reason);
        self
    }

    pub(crate) fn text(mut self, text: &str) -> Self {
        self.text = Some(text.to_string());
        self
    }

    #[cfg(test)]
    fn parse(message: &Message) -> Self {
        Self {
            #[allow(clippy::expect_used)]
            ref_seq_num: message
                .get(REF_SEQ_NUM)
                .expect("ref_seq_num should be present"),
            ref_tag_id: message.get(REF_TAG_ID).ok(),
            ref_msg_type: message.get(REF_MSG_TYPE).ok(),
            session_reject_reason: message.get(SESSION_REJECT_REASON).ok(),
            text: message.get::<&str>(TEXT).ok().map(|s| s.to_string()),
        }
    }
}

impl OutboundMessage for Reject {
    fn write(&self, msg: &mut Message) {
        msg.set(REF_SEQ_NUM, self.ref_seq_num);

        if let Some(ref_tag_id) = self.ref_tag_id {
            msg.set(REF_TAG_ID, ref_tag_id);
        }
        if let Some(ref_msg_type) = self.ref_msg_type {
            msg.set(REF_MSG_TYPE, ref_msg_type);
        }
        if let Some(session_reject_reason) = self.session_reject_reason {
            msg.set(SESSION_REJECT_REASON, session_reject_reason);
        }
        if let Some(text) = &self.text {
            msg.set(TEXT, text.as_str());
        }
    }

    fn message_type(&self) -> &str {
        Self::MSG_TYPE
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use hotfix_message::message::Message;

    #[test]
    fn test_write_reject_with_required_fields_only() {
        let reject = Reject::new(123);

        let mut msg = Message::new("FIX.4.4", "3");
        reject.write(&mut msg);

        assert_eq!(msg.get::<u64>(REF_SEQ_NUM).unwrap(), 123);
        assert!(msg.get::<u64>(REF_TAG_ID).is_err());
        assert!(msg.get::<MsgType>(REF_MSG_TYPE).is_err());
        assert!(
            msg.get::<SessionRejectReason>(SESSION_REJECT_REASON)
                .is_err()
        );
        assert!(msg.get::<&str>(TEXT).is_err());
    }

    #[test]
    fn test_write_reject_with_all_fields() {
        let reject = Reject::new(456)
            .ref_tag_id(35)
            .ref_msg_type(MsgType::ExecutionReport)
            .session_reject_reason(SessionRejectReason::InvalidTagNumber)
            .text("Invalid message format");

        let mut msg = Message::new("FIX.4.4", "3");
        reject.write(&mut msg);

        assert_eq!(msg.get::<u64>(REF_SEQ_NUM).unwrap(), 456);
        assert_eq!(msg.get::<u64>(REF_TAG_ID).unwrap(), 35);
        assert_eq!(
            msg.get::<MsgType>(REF_MSG_TYPE).unwrap(),
            MsgType::ExecutionReport
        );
        assert_eq!(
            msg.get::<SessionRejectReason>(SESSION_REJECT_REASON)
                .unwrap(),
            SessionRejectReason::InvalidTagNumber
        );
        assert_eq!(msg.get::<&str>(TEXT).unwrap(), "Invalid message format");
    }

    #[test]
    fn test_parse_reject_with_required_fields_only() {
        let mut msg = Message::new("FIX.4.4", "3");
        msg.set(REF_SEQ_NUM, 999u64);

        let parsed = Reject::parse(&msg);

        assert_eq!(parsed.ref_seq_num, 999);
        assert!(parsed.ref_tag_id.is_none());
        assert!(parsed.ref_msg_type.is_none());
        assert!(parsed.session_reject_reason.is_none());
        assert!(parsed.text.is_none());
    }

    #[test]
    fn test_parse_reject_with_all_fields() {
        let mut msg = Message::new("FIX.4.4", "3");
        msg.set(REF_SEQ_NUM, 777u64);
        msg.set(REF_TAG_ID, 40u64);
        msg.set(REF_MSG_TYPE, MsgType::OrderSingle);
        msg.set(
            SESSION_REJECT_REASON,
            SessionRejectReason::TagNotDefinedForThisMessageType,
        );
        msg.set(TEXT, "Field not allowed");

        let parsed = Reject::parse(&msg);

        assert_eq!(parsed.ref_seq_num, 777);
        assert_eq!(parsed.ref_tag_id, Some(40));
        assert_eq!(parsed.ref_msg_type, Some(MsgType::OrderSingle));
        assert_eq!(
            parsed.session_reject_reason,
            Some(SessionRejectReason::TagNotDefinedForThisMessageType)
        );
        assert_eq!(parsed.text, Some("Field not allowed".to_string()));
    }

    #[test]
    fn test_round_trip_serialization() {
        let original = Reject::new(555)
            .ref_tag_id(44)
            .ref_msg_type(MsgType::OrderCancelRequest)
            .session_reject_reason(SessionRejectReason::ValueIsIncorrect)
            .text("Price field is invalid");

        let mut msg = Message::new("FIX.4.4", "3");
        original.write(&mut msg);

        let parsed = Reject::parse(&msg);

        assert_eq!(parsed.ref_seq_num, original.ref_seq_num);
        assert_eq!(parsed.ref_tag_id, original.ref_tag_id);
        assert_eq!(parsed.ref_msg_type, original.ref_msg_type);
        assert_eq!(parsed.session_reject_reason, original.session_reject_reason);
        assert_eq!(parsed.text, original.text);
    }

    #[test]
    fn test_round_trip_with_minimal_fields() {
        let original = Reject::new(111);

        let mut msg = Message::new("FIX.4.4", "3");
        original.write(&mut msg);

        let parsed = Reject::parse(&msg);

        assert_eq!(parsed.ref_seq_num, original.ref_seq_num);
        assert_eq!(parsed.ref_tag_id, original.ref_tag_id);
        assert_eq!(parsed.ref_msg_type, original.ref_msg_type);
        assert_eq!(parsed.session_reject_reason, original.session_reject_reason);
        assert_eq!(parsed.text, original.text);
    }
}