Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
490 changes: 431 additions & 59 deletions crates/hotfix-message/src/builder.rs

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions crates/hotfix-message/src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ mod tests {
msg.set(fix44::PRICE, 150);
msg.set(fix44::ORDER_QTY, 60);

let config = Config { separator: b'|' };
let config = Config::with_separator(b'|');
let raw_message = msg.encode(&config)?;

let builder = MessageBuilder::new(Dictionary::fix44(), config)?;
Expand Down Expand Up @@ -146,7 +146,7 @@ mod tests {
party_2.store_field(Field::new(fix44::PARTY_ROLE.tag(), b"2".to_vec()));

msg.body.set_groups(vec![party_1, party_2])?;
let config = Config { separator: b'|' };
let config = Config::with_separator(b'|');
let raw_message = msg.encode(&config)?;

let builder = MessageBuilder::new(Dictionary::fix44(), config)?;
Expand Down
5 changes: 5 additions & 0 deletions crates/hotfix-message/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@ pub enum ParserError {
InvalidComponent(String),
#[error("MsgType {0} is not a valid message type")]
InvalidMsgType(String),
#[error(
"required field (tag = {tag}) is missing{}",
match group_tag { Some(g) => format!(" from repeating group (group tag = {g})"), None => String::new() }
)]
RequiredFieldMissing { tag: u32, group_tag: Option<u32> },
#[error("malformed message: {0}")]
Malformed(String),
}
Expand Down
16 changes: 14 additions & 2 deletions crates/hotfix-message/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,16 +119,28 @@ impl Part for Message {
#[derive(Clone, Copy)]
pub struct Config {
pub(crate) separator: u8,
pub(crate) validate_user_defined_fields: bool,
}

impl Config {
pub const fn with_separator(separator: u8) -> Self {
Self { separator }
Self {
separator,
validate_user_defined_fields: true,
}
}

pub const fn validate_user_defined_fields(mut self, value: bool) -> Self {
self.validate_user_defined_fields = value;
self
}
}

impl Default for Config {
fn default() -> Self {
Self { separator: SOH }
Self {
separator: SOH,
validate_user_defined_fields: true,
}
}
}
1 change: 1 addition & 0 deletions crates/hotfix-message/src/parsed_message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub enum InvalidReason {
InvalidOrderInGroup { tag: u32, group_tag: u32 },
InvalidComponent(String),
InvalidMsgType(String),
RequiredFieldMissing { tag: u32, group_tag: Option<u32> },
}

#[derive(Debug)]
Expand Down
97 changes: 97 additions & 0 deletions crates/hotfix/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,63 @@ pub struct SessionConfig {

/// The schedule configuration for the session
pub schedule: Option<ScheduleConfig>,

/// The validation configuration for the session
#[serde(default)]
pub validation: ValidationConfig,
}

#[derive(Clone, Debug, Deserialize)]
/// The configuration of validation rules.
pub struct ValidationConfig {
/// Specifies whether unknown user-defined tags (>= 5000) should cause the message to be rejected.
#[serde(default = "default_true")]
pub validate_user_defined_fields: bool,
}

impl ValidationConfig {
pub fn builder() -> VerificationConfigBuilder {
VerificationConfigBuilder::default()
}
}

impl Default for ValidationConfig {
fn default() -> Self {
VerificationConfigBuilder::default().build()
}
}

pub struct VerificationConfigBuilder {
validate_user_defined_fields: bool,
}

impl Default for VerificationConfigBuilder {
fn default() -> Self {
Self {
validate_user_defined_fields: true,
}
}
}

impl VerificationConfigBuilder {
pub fn new() -> Self {
Self::default()
}

pub fn validate_user_defined_fields(mut self, value: bool) -> Self {
self.validate_user_defined_fields = value;
self
}

pub fn build(self) -> ValidationConfig {
ValidationConfig {
validate_user_defined_fields: self.validate_user_defined_fields,
}
}
}

fn default_true() -> bool {
true
}

/// Errors that may occur when loading configuration.
Expand Down Expand Up @@ -170,6 +227,7 @@ reset_on_logon = false
assert_eq!(session_config.tls_config, Some(expected_tls_config));
assert_eq!(session_config.reconnect_interval, 30);
assert_eq!(session_config.logon_timeout, 10);
assert!(session_config.validation.validate_user_defined_fields);
}

#[test]
Expand Down Expand Up @@ -439,6 +497,45 @@ end_day = "Friday"
assert_eq!(session_config.reconnect_interval, 15);
}

#[test]
fn test_verification_config_defaults_when_omitted() {
let config_contents = r#"
[[sessions]]
begin_string = "FIX.4.4"
sender_comp_id = "send-comp-id"
target_comp_id = "target-comp-id"
connection_port = 443
connection_host = "127.0.0.1"
heartbeat_interval = 30
"#;

let config: Config = toml::from_str(config_contents).unwrap();
let session_config = config.sessions.first().unwrap();

assert!(session_config.validation.validate_user_defined_fields);
}

#[test]
fn test_verification_config_can_disable_user_defined_field_validation() {
let config_contents = r#"
[[sessions]]
begin_string = "FIX.4.4"
sender_comp_id = "send-comp-id"
target_comp_id = "target-comp-id"
connection_port = 443
connection_host = "127.0.0.1"
heartbeat_interval = 30

[sessions.validation]
validate_user_defined_fields = false
"#;

let config: Config = toml::from_str(config_contents).unwrap();
let session_config = config.sessions.first().unwrap();

assert!(!session_config.validation.validate_user_defined_fields);
}

#[test]
fn test_load_from_path_success() {
let config_contents = r#"
Expand Down
1 change: 1 addition & 0 deletions crates/hotfix/src/initiator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ mod tests {
reconnect_interval: 1, // Short for tests
reset_on_logon: false,
schedule: None,
validation: Default::default(),
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/hotfix/src/message/verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ fn check_target_comp_id(
#[cfg(test)]
mod tests {
use super::{Message, SessionConfig, VerificationFlags, verify_message};
use crate::config::ValidationConfig;
use crate::message::sequence_reset::SequenceReset;
use crate::message::verification_issue::{CompIdType, MessageError, VerificationIssue};
use hotfix_message::field_types::Timestamp;
Expand All @@ -258,6 +259,7 @@ mod tests {
reconnect_interval: 0,
reset_on_logon: false,
schedule: None,
validation: ValidationConfig::default(),
}
}

Expand Down
26 changes: 25 additions & 1 deletion crates/hotfix/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ where
let schedule_check_timer = sleep(Duration::from_secs(SCHEDULE_CHECK_INTERVAL));

let dictionary = Self::get_data_dictionary(&config)?;
let message_config = MessageConfig::default();
let message_config = MessageConfig::default()
.validate_user_defined_fields(config.validation.validate_user_defined_fields);
let message_builder = MessageBuilder::new(dictionary, message_config)?;
let schedule = config.schedule.as_ref().try_into()?;
let ctx = SessionCtx {
Expand Down Expand Up @@ -184,6 +185,27 @@ where
}
}
}
InvalidReason::RequiredFieldMissing { tag, group_tag } => {
match message.header().get(MSG_SEQ_NUM) {
Ok(msg_seq_num) => {
let text = match group_tag {
Some(group_tag) => {
format!("required tag missing: {tag} (in group {group_tag})")
}
None => format!("required tag missing: {tag}"),
};
let reject = Reject::new(msg_seq_num)
.session_reject_reason(SessionRejectReason::RequiredTagMissing)
.text(&text);
self.send_message(reject)
.await
.with_send_context("reject for missing required field")?;
}
Err(err) => {
error!("failed to get message seq num: {:?}", err);
}
}
}
},
ParsedMessage::UnexpectedError(err) => {
error!("unexpected error: {:?}", err);
Expand Down Expand Up @@ -771,6 +793,7 @@ async fn run_session<App, Store>(
mod tests {
use super::*;
use crate::application::{InboundDecision, OutboundDecision};
use crate::config::ValidationConfig;
use crate::message::OutboundMessage;
use crate::store::{Result as StoreResult, StoreError};
use chrono::{DateTime, Datelike, NaiveDate, NaiveTime, TimeDelta, Timelike};
Expand Down Expand Up @@ -907,6 +930,7 @@ mod tests {
reconnect_interval: 30,
reset_on_logon: false,
schedule: None,
validation: ValidationConfig::default(),
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/hotfix/src/session/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ pub(crate) fn create_test_ctx(store: FakeMessageStore) -> SessionCtx<(), FakeMes
reconnect_interval: 30,
reset_on_logon: false,
schedule: None,
validation: Default::default(),
},
store,
application: (),
Expand Down
1 change: 1 addition & 0 deletions crates/hotfix/tests/connection_test_cases/connect_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fn create_session_config(host: &str, port: u16, tls_config: Option<TlsConfig>) -
reconnect_interval: 30,
reset_on_logon: false,
schedule: None,
validation: Default::default(),
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/hotfix/tests/session_test_cases/common/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ pub fn create_session_config() -> SessionConfig {
reconnect_interval: 30,
reset_on_logon: false,
schedule: None,
validation: Default::default(),
}
}

Expand Down
3 changes: 2 additions & 1 deletion examples/load-testing/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ mod messages;

use anyhow::Result;
use clap::{Parser, ValueEnum};
use hotfix::config::SessionConfig;
use hotfix::config::{SessionConfig, ValidationConfig};
use hotfix::field_types::{Date, Timestamp};
use hotfix::fix44;
use hotfix::fix44::OrdType;
Expand Down Expand Up @@ -168,5 +168,6 @@ fn get_config() -> SessionConfig {
reconnect_interval: 30,
reset_on_logon: true,
schedule: None,
validation: ValidationConfig::default(),
}
}
Loading