forgefix 0.3.0

ForgeFIX is an opinionated FIX 4.2 client library for the buy-side written in Rust. ForgeFIX is optimized for the subset of the FIX protocol used by buy-side firms connecting to brokers and exchanges for communicating orders and fills.
Documentation
use crate::fix::checksum::checksum_is_valid;
use crate::fix::fields::{MsgType, SessionRejectReason, Tags};
use crate::fix::mem::MsgBuf;
use crate::fix::{GarbledMessageType, SessionError};

use chrono::{DateTime, Duration, Utc};

#[allow(clippy::too_many_arguments)]
pub(super) fn validate_msg<'a>(
    expected_sender_comp_id: &str,
    expected_target_comp_id: &str,
    msg_type: char,
    msg_seq_num: u32,
    target_comp_id: Option<&'a [u8]>,
    sender_comp_id: Option<&'a [u8]>,
    sending_time: Option<DateTime<Utc>>,
    poss_dup_flag: Option<char>,
    orig_sending_time: Option<DateTime<Utc>>,
    begin_seq_no: Option<u32>,
    end_seq_no: Option<u32>,
) -> Result<(), SessionError> {
    if <char as TryInto<MsgType>>::try_into(msg_type).is_err() {
        return Err(SessionError::new_message_rejected(
            Some(SessionRejectReason::INVALID_MSGTYPE),
            msg_seq_num,
            Some(Tags::MsgType.into()),
            Some(msg_type),
        ));
    }
    if Some(expected_target_comp_id.as_bytes()) != target_comp_id {
        return Err(SessionError::new_message_rejected(
            Some(SessionRejectReason::COMPID_PROBLEM),
            msg_seq_num,
            Some(Tags::TargetCompID.into()),
            Some(msg_type),
        ));
    }

    if Some(expected_sender_comp_id.as_bytes()) != sender_comp_id {
        return Err(SessionError::new_message_rejected(
            Some(SessionRejectReason::COMPID_PROBLEM),
            msg_seq_num,
            Some(Tags::SenderCompID.into()),
            Some(msg_type),
        ));
    }

    if sending_time.is_none() {
        return Err(SessionError::new_message_rejected(
            Some(SessionRejectReason::REQUIRED_TAG_MISSING),
            msg_seq_num,
            Some(Tags::SendingTime.into()),
            Some(msg_type),
        ));
    }

    if !valid_sending_time(sending_time.unwrap(), Duration::seconds(10)) {
        return Err(SessionError::new_message_rejected(
            Some(SessionRejectReason::SENDINGTIME_ACCURACY_PROBLEM),
            msg_seq_num,
            Some(Tags::SendingTime.into()),
            Some(msg_type),
        ));
    }

    match poss_dup_flag {
        Some('Y') => {
            validate_duplicate(
                msg_seq_num,
                msg_type,
                sending_time.unwrap(),
                orig_sending_time,
            )?;
        }
        Some('N') | None => {}
        Some(_) => {
            return Err(SessionError::new_message_rejected(
                Some(SessionRejectReason::VALUE_IS_INCORRECT),
                msg_seq_num,
                Some(Tags::PossDupFlag.into()),
                Some(msg_type),
            ));
        }
    }

    if msg_type == MsgType::RESEND_REQUEST.into() && !valid_resend_request(begin_seq_no, end_seq_no)
    {
        return Err(SessionError::new_message_rejected(
            Some(SessionRejectReason::REQUIRED_TAG_MISSING),
            msg_seq_num,
            None,
            Some(msg_type),
        ));
    }

    Ok(())
}

fn valid_resend_request(begin_seq_no: Option<u32>, end_seq_no: Option<u32>) -> bool {
    begin_seq_no.is_some() && end_seq_no.is_some()
}

pub(super) fn validate_checksum(msg_buf: &MsgBuf) -> Result<(), SessionError> {
    if !checksum_is_valid(&msg_buf.0) {
        return Err(SessionError::new_garbled_message(
            String::from("Checksum invalid"),
            GarbledMessageType::ChecksumIssue,
        ));
    }
    Ok(())
}

pub(super) fn validate_msg_length(msg_buf: &[u8], msg_length: usize) -> Result<(), SessionError> {
    if &msg_buf[msg_length - 7..msg_length - 4] != b"10=".as_slice() {
        return Err(SessionError::GarbledMessage {
            text: String::from("BodyLength(9) was incorrect"),
            garbled_msg_type: GarbledMessageType::BodyLengthIssue,
        });
    }
    Ok(())
}

fn valid_sending_time(sending_time: DateTime<Utc>, sending_time_threshold: Duration) -> bool {
    Utc::now() - sending_time < sending_time_threshold
        && sending_time - Utc::now() < sending_time_threshold
}

fn validate_duplicate(
    msg_seq_num: u32,
    msg_type: char,
    sending_time: DateTime<Utc>,
    orig_sending_time: Option<DateTime<Utc>>,
) -> Result<(), SessionError> {
    if orig_sending_time.is_none() {
        return Err(SessionError::new_message_rejected(
            Some(SessionRejectReason::REQUIRED_TAG_MISSING),
            msg_seq_num,
            Some(Tags::OrigSendingTime.into()),
            Some(msg_type),
        ));
    }

    if orig_sending_time.unwrap() > sending_time {
        return Err(SessionError::new_message_rejected(
            Some(SessionRejectReason::SENDINGTIME_ACCURACY_PROBLEM),
            msg_seq_num,
            None,
            Some(msg_type),
        ));
    }

    Ok(())
}