1use std::collections::HashMap;
7use std::time::Duration;
8
9use crate::datadictionary::ValidationSettings;
10use crate::error::{Error, Result};
11use crate::schedule::Schedule;
12use crate::session_id::SessionId;
13use crate::value::TimestampPrecision;
14
15#[derive(Debug, Clone, Default)]
16pub struct Settings {
17 pub defaults: HashMap<String, String>,
18 pub sessions: Vec<HashMap<String, String>>,
19}
20
21impl Settings {
22 pub fn parse(text: &str) -> Result<Self> {
23 let mut settings = Settings::default();
24 let mut current: Option<HashMap<String, String>> = None;
25 let mut in_default = false;
26
27 for (line_no, line) in text.lines().enumerate() {
28 let line = line.trim();
29 if line.is_empty() || line.starts_with('#') {
30 continue;
31 }
32 if line.starts_with('[') && line.ends_with(']') {
33 if let Some(section) = current.take() {
34 settings.sessions.push(section);
35 }
36 let name = line[1..line.len() - 1].trim().to_ascii_uppercase();
37 match name.as_str() {
38 "DEFAULT" => in_default = true,
39 "SESSION" => {
40 in_default = false;
41 current = Some(HashMap::new());
42 }
43 other => {
44 return Err(Error::Config(format!(
45 "line {}: unknown section [{other}]",
46 line_no + 1
47 )));
48 }
49 }
50 continue;
51 }
52 let Some((key, value)) = line.split_once('=') else {
53 return Err(Error::Config(format!(
54 "line {}: expected key=value, got {line:?}",
55 line_no + 1
56 )));
57 };
58 let (key, value) = (key.trim().to_owned(), value.trim().to_owned());
59 if let Some(section) = current.as_mut() {
60 section.insert(key, value);
61 } else if in_default {
62 settings.defaults.insert(key, value);
63 } else {
64 return Err(Error::Config(format!(
65 "line {}: key outside of [DEFAULT]/[SESSION]",
66 line_no + 1
67 )));
68 }
69 }
70 if let Some(section) = current.take() {
71 settings.sessions.push(section);
72 }
73 Ok(settings)
74 }
75
76 pub async fn from_file(path: impl AsRef<std::path::Path>) -> Result<Self> {
77 let text = tokio::fs::read_to_string(path).await?;
78 Self::parse(&text)
79 }
80
81 pub fn session_configs(&self) -> Result<Vec<SessionConfig>> {
84 self.sessions
85 .iter()
86 .map(|s| {
87 let mut merged = self.defaults.clone();
88 merged.extend(s.iter().map(|(k, v)| (k.clone(), v.clone())));
89 SessionConfig::from_map(&merged)
90 })
91 .collect()
92 }
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq)]
96pub enum ConnectionType {
97 Initiator,
98 Acceptor,
99}
100
101#[derive(Debug, Clone, Default)]
103pub struct TlsSettings {
104 pub enabled: bool,
106 pub certificate_file: Option<String>,
110 pub private_key_file: Option<String>,
112 pub ca_file: Option<String>,
117 pub insecure_skip_verify: bool,
120 pub server_name: Option<String>,
122}
123
124#[derive(Debug, Clone)]
125pub struct SessionConfig {
126 pub session_id: SessionId,
127 pub connection_type: ConnectionType,
128 pub heart_bt_int: Duration,
131 pub heart_bt_int_override: Option<Duration>,
132 pub socket_connect_host: String,
133 pub socket_connect_port: u16,
134 pub socket_accept_port: u16,
135 pub reconnect_interval: Duration,
136 pub logon_timeout: Duration,
137 pub logout_timeout: Duration,
138 pub reset_on_logon: bool,
139 pub reset_on_logout: bool,
140 pub reset_on_disconnect: bool,
141 pub refresh_on_logon: bool,
142 pub send_reset_seq_num_flag: bool,
144 pub persist_messages: bool,
145 pub check_comp_id: bool,
146 pub check_latency: bool,
147 pub max_latency: Duration,
148 pub send_redundant_resend_requests: bool,
149 pub timestamp_precision: TimestampPrecision,
150 pub validate_length_checksum: bool,
151 pub use_data_dictionary: bool,
152 pub data_dictionary: Option<String>,
153 pub transport_data_dictionary: Option<String>,
155 pub app_data_dictionary: Option<String>,
157 pub validation: ValidationSettings,
158 pub enable_last_msg_seq_num_processed: bool,
160 pub max_messages_in_resend_request: u64,
163 pub send_logout_before_disconnect_from_timeout: bool,
165 pub requires_orig_sending_time: bool,
167 pub send_next_expected_msg_seq_num: bool,
171 pub tls: TlsSettings,
173 pub schedule: Schedule,
177 pub logon_schedule: Schedule,
180 pub file_store_path: Option<String>,
181 pub file_log_path: Option<String>,
182 pub default_appl_ver_id: Option<String>,
184}
185
186fn get_bool(m: &HashMap<String, String>, key: &str, default: bool) -> Result<bool> {
187 match m.get(key).map(|s| s.as_str()) {
188 None => Ok(default),
189 Some("Y") => Ok(true),
190 Some("N") => Ok(false),
191 Some(other) => Err(Error::Config(format!("{key} must be Y or N, got {other:?}"))),
192 }
193}
194
195fn get_u64(m: &HashMap<String, String>, key: &str, default: u64) -> Result<u64> {
196 match m.get(key) {
197 None => Ok(default),
198 Some(v) => v.parse().map_err(|_| Error::Config(format!("{key} must be a number"))),
199 }
200}
201
202impl SessionConfig {
203 pub fn from_map(m: &HashMap<String, String>) -> Result<Self> {
204 let require = |key: &str| {
205 m.get(key)
206 .cloned()
207 .ok_or_else(|| Error::Config(format!("missing required setting {key}")))
208 };
209
210 let begin_string = require("BeginString")?;
211 const VALID: &[&str] =
212 &["FIX.4.0", "FIX.4.1", "FIX.4.2", "FIX.4.3", "FIX.4.4", "FIXT.1.1"];
213 if !VALID.contains(&begin_string.as_str()) {
214 return Err(Error::Config(format!("unsupported BeginString {begin_string:?}")));
215 }
216
217 let session_id = SessionId {
218 begin_string: begin_string.clone(),
219 sender_comp_id: require("SenderCompID")?,
220 sender_sub_id: m.get("SenderSubID").cloned().unwrap_or_default(),
221 sender_location_id: m.get("SenderLocationID").cloned().unwrap_or_default(),
222 target_comp_id: require("TargetCompID")?,
223 target_sub_id: m.get("TargetSubID").cloned().unwrap_or_default(),
224 target_location_id: m.get("TargetLocationID").cloned().unwrap_or_default(),
225 qualifier: m.get("SessionQualifier").cloned().unwrap_or_default(),
226 };
227
228 let connection_type = match require("ConnectionType")?.as_str() {
229 "initiator" => ConnectionType::Initiator,
230 "acceptor" => ConnectionType::Acceptor,
231 other => {
232 return Err(Error::Config(format!(
233 "ConnectionType must be initiator or acceptor, got {other:?}"
234 )));
235 }
236 };
237
238 let use_local = get_bool(m, "UseLocalTime", false)?;
240 let non_stop = get_bool(m, "NonStopSession", false)?;
241 let str_opt = |k: &str| m.get(k).map(|s| s.as_str());
242 let schedule = Schedule::parse(
243 str_opt("StartTime"),
244 str_opt("EndTime"),
245 str_opt("StartDay"),
246 str_opt("EndDay"),
247 use_local,
248 non_stop,
249 )?;
250 let logon_schedule = if m.contains_key("LogonTime") || m.contains_key("LogoutTime") {
251 Schedule::parse(
252 str_opt("LogonTime"),
253 str_opt("LogoutTime"),
254 str_opt("LogonDay").or(str_opt("StartDay")),
255 str_opt("LogoutDay").or(str_opt("EndDay")),
256 use_local,
257 non_stop,
258 )?
259 } else {
260 schedule.clone()
261 };
262
263 let heart_bt_int = match connection_type {
264 ConnectionType::Initiator => {
265 let secs: u64 = require("HeartBtInt")?
266 .parse()
267 .map_err(|_| Error::Config("HeartBtInt must be a number".into()))?;
268 if secs == 0 {
269 return Err(Error::Config("HeartBtInt must be > 0".into()));
270 }
271 Duration::from_secs(secs)
272 }
273 ConnectionType::Acceptor => Duration::from_secs(get_u64(m, "HeartBtInt", 30)?),
274 };
275
276 let (socket_connect_host, socket_connect_port, socket_accept_port) = match connection_type {
277 ConnectionType::Initiator => (
278 require("SocketConnectHost")?,
279 require("SocketConnectPort")?
280 .parse()
281 .map_err(|_| Error::Config("SocketConnectPort must be a port".into()))?,
282 0,
283 ),
284 ConnectionType::Acceptor => (
285 String::new(),
286 0,
287 require("SocketAcceptPort")?
288 .parse()
289 .map_err(|_| Error::Config("SocketAcceptPort must be a port".into()))?,
290 ),
291 };
292
293 let default_precision = if begin_string.as_str() < "FIX.4.2" {
295 TimestampPrecision::Seconds
296 } else {
297 TimestampPrecision::Millis
298 };
299 let timestamp_precision = match m.get("TimestampPrecision").map(|s| s.as_str()) {
300 None => match get_bool(m, "MillisecondsInTimeStamp", true)? {
301 true => default_precision,
302 false => TimestampPrecision::Seconds,
303 },
304 Some("0") => TimestampPrecision::Seconds,
305 Some("3") => TimestampPrecision::Millis,
306 Some("6") => TimestampPrecision::Micros,
307 Some("9") => TimestampPrecision::Nanos,
308 Some(other) => {
309 return Err(Error::Config(format!(
310 "TimestampPrecision must be 0, 3, 6 or 9, got {other:?}"
311 )));
312 }
313 };
314
315 if session_id.is_fixt() && !m.contains_key("DefaultApplVerID") {
316 return Err(Error::Config("FIXT.1.1 sessions require DefaultApplVerID".into()));
317 }
318
319 Ok(Self {
320 connection_type,
321 heart_bt_int,
322 heart_bt_int_override: m
323 .get("HeartBtIntOverride")
324 .map(|v| {
325 v.parse::<u64>()
326 .map(Duration::from_secs)
327 .map_err(|_| Error::Config("HeartBtIntOverride must be a number".into()))
328 })
329 .transpose()?,
330 socket_connect_host,
331 socket_connect_port,
332 socket_accept_port,
333 reconnect_interval: Duration::from_secs(get_u64(m, "ReconnectInterval", 30)?),
334 logon_timeout: Duration::from_secs(get_u64(m, "LogonTimeout", 10)?),
335 logout_timeout: Duration::from_secs(get_u64(m, "LogoutTimeout", 2)?),
336 reset_on_logon: get_bool(m, "ResetOnLogon", false)?,
337 reset_on_logout: get_bool(m, "ResetOnLogout", false)?,
338 reset_on_disconnect: get_bool(m, "ResetOnDisconnect", false)?,
339 refresh_on_logon: get_bool(m, "RefreshOnLogon", false)?,
340 send_reset_seq_num_flag: get_bool(m, "SendResetSeqNumFlag", false)?,
341 persist_messages: get_bool(m, "PersistMessages", true)?,
342 check_comp_id: get_bool(m, "CheckCompID", true)?,
343 check_latency: get_bool(m, "CheckLatency", true)?,
344 max_latency: Duration::from_secs(get_u64(m, "MaxLatency", 120)?),
345 send_redundant_resend_requests: get_bool(m, "SendRedundantResendRequests", false)?,
346 timestamp_precision,
347 validate_length_checksum: get_bool(m, "ValidateLengthAndChecksum", true)?,
348 use_data_dictionary: get_bool(m, "UseDataDictionary", true)?,
349 data_dictionary: m.get("DataDictionary").cloned(),
350 transport_data_dictionary: m.get("TransportDataDictionary").cloned(),
351 app_data_dictionary: m.get("AppDataDictionary").cloned(),
352 enable_last_msg_seq_num_processed: get_bool(m, "EnableLastMsgSeqNumProcessed", false)?,
353 max_messages_in_resend_request: get_u64(m, "MaxMessagesInResendRequest", 0)?,
354 send_logout_before_disconnect_from_timeout: get_bool(
355 m,
356 "SendLogoutBeforeDisconnectFromTimeout",
357 false,
358 )?,
359 requires_orig_sending_time: get_bool(m, "RequiresOrigSendingTime", true)?,
360 send_next_expected_msg_seq_num: get_bool(m, "SendNextExpectedMsgSeqNum", false)?
361 || get_bool(m, "EnableNextExpectedMsgSeqNum", false)?,
362 tls: TlsSettings {
363 enabled: get_bool(m, "SocketUseSSL", false)?,
364 certificate_file: m.get("SocketCertificateFile").cloned(),
365 private_key_file: m.get("SocketPrivateKeyFile").cloned(),
366 ca_file: m.get("SocketCAFile").cloned(),
367 insecure_skip_verify: get_bool(m, "SocketInsecureSkipVerify", false)?,
368 server_name: m.get("SocketServerName").cloned(),
369 },
370 schedule,
371 logon_schedule,
372 validation: ValidationSettings {
373 check_fields_out_of_order: get_bool(m, "ValidateFieldsOutOfOrder", true)?,
374 check_fields_have_values: get_bool(m, "ValidateFieldsHaveValues", true)?,
375 check_user_defined_fields: get_bool(m, "ValidateUserDefinedFields", true)?,
376 allow_unknown_message_fields: get_bool(m, "AllowUnknownMsgFields", false)?,
377 },
378 file_store_path: m.get("FileStorePath").cloned(),
379 file_log_path: m.get("FileLogPath").cloned(),
380 default_appl_ver_id: m.get("DefaultApplVerID").cloned(),
381 session_id,
382 })
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 const SAMPLE: &str = r#"
391# comment
392[DEFAULT]
393ConnectionType=initiator
394ReconnectInterval=15
395SocketConnectHost=127.0.0.1
396
397[SESSION]
398BeginString=FIX.4.2
399SenderCompID=CLIENT1
400TargetCompID=EXEC
401HeartBtInt=30
402SocketConnectPort=9876
403
404[SESSION]
405BeginString=FIX.4.4
406SenderCompID=CLIENT1
407TargetCompID=EXEC2
408HeartBtInt=20
409SocketConnectPort=9877
410ResetOnLogon=Y
411"#;
412
413 #[test]
414 fn parses_sessions_with_default_overlay() {
415 let settings = Settings::parse(SAMPLE).unwrap();
416 let configs = settings.session_configs().unwrap();
417 assert_eq!(configs.len(), 2);
418
419 let c = &configs[0];
420 assert_eq!(c.session_id.to_string(), "FIX.4.2:CLIENT1->EXEC");
421 assert_eq!(c.connection_type, ConnectionType::Initiator);
422 assert_eq!(c.heart_bt_int, Duration::from_secs(30));
423 assert_eq!(c.reconnect_interval, Duration::from_secs(15));
424 assert_eq!(c.socket_connect_host, "127.0.0.1");
425 assert_eq!(c.socket_connect_port, 9876);
426 assert!(!c.reset_on_logon);
427
428 let c = &configs[1];
429 assert!(c.reset_on_logon);
430 assert_eq!(c.timestamp_precision, TimestampPrecision::Millis);
431 }
432
433 #[test]
434 fn missing_required_key_errors() {
435 let settings = Settings::parse(
436 "[SESSION]\nBeginString=FIX.4.2\nSenderCompID=A\nConnectionType=initiator\n",
437 )
438 .unwrap();
439 assert!(settings.session_configs().is_err());
440 }
441
442 #[test]
443 fn old_fix_defaults_to_second_precision() {
444 let text = "[SESSION]\nConnectionType=acceptor\nBeginString=FIX.4.0\nSenderCompID=A\nTargetCompID=B\nSocketAcceptPort=5001\n";
445 let c = &Settings::parse(text).unwrap().session_configs().unwrap()[0];
446 assert_eq!(c.timestamp_precision, TimestampPrecision::Seconds);
447 }
448}