use crate::acceptor::Acceptor;
use crate::application::{Application, NoOpApplication};
use crate::error::EngineError;
use crate::initiator::Initiator;
use ironfix_session::config::SessionConfig;
use std::sync::Arc;
use std::time::Duration;
#[derive(Debug)]
pub struct EngineBuilder<A: Application = NoOpApplication> {
application: Arc<A>,
sessions: Vec<SessionConfig>,
connect_timeout: Duration,
}
impl Default for EngineBuilder<NoOpApplication> {
fn default() -> Self {
Self::new()
}
}
impl EngineBuilder<NoOpApplication> {
#[must_use]
pub fn new() -> Self {
Self {
application: Arc::new(NoOpApplication),
sessions: Vec::new(),
connect_timeout: Duration::from_secs(30),
}
}
}
impl<A: Application + 'static> EngineBuilder<A> {
#[must_use]
pub fn with_application<B: Application>(self, application: B) -> EngineBuilder<B> {
EngineBuilder {
application: Arc::new(application),
sessions: self.sessions,
connect_timeout: self.connect_timeout,
}
}
#[must_use]
pub fn add_session(mut self, config: SessionConfig) -> Self {
self.sessions.push(config);
self
}
#[must_use]
pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
self.connect_timeout = timeout;
self
}
#[must_use]
pub fn sessions(&self) -> &[SessionConfig] {
&self.sessions
}
#[must_use]
pub const fn connect_timeout(&self) -> Duration {
self.connect_timeout
}
#[must_use]
pub fn application(&self) -> Arc<A> {
Arc::clone(&self.application)
}
pub fn into_initiator(self) -> Result<Initiator<A>, EngineError> {
let config = self.single_session()?;
Ok(Initiator::new(config, self.application).with_connect_timeout(self.connect_timeout))
}
pub fn into_acceptor(self) -> Result<Acceptor<A>, EngineError> {
let config = self.single_session()?;
Ok(Acceptor::new(config, self.application))
}
fn single_session(&self) -> Result<SessionConfig, EngineError> {
match self.sessions.as_slice() {
[config] => Ok(config.clone()),
[] => Err(EngineError::Configuration(
"no session configured: add exactly one with add_session".to_string(),
)),
many => Err(EngineError::Configuration(format!(
"{} sessions configured, but a single-session engine requires exactly one",
many.len()
))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use ironfix_core::types::CompId;
#[track_caller]
fn comp_id(value: &str) -> CompId {
match CompId::new(value) {
Ok(id) => id,
Err(err) => panic!("test CompId must be valid: {err}"),
}
}
fn session() -> SessionConfig {
SessionConfig::new(comp_id("SENDER"), comp_id("TARGET"), "FIX.4.4")
}
#[test]
fn test_engine_builder_default_is_empty() {
let builder = EngineBuilder::new();
assert_eq!(builder.connect_timeout(), Duration::from_secs(30));
assert!(builder.sessions().is_empty());
}
#[test]
fn test_engine_builder_add_session_records_it() {
let builder = EngineBuilder::new()
.add_session(session())
.with_connect_timeout(Duration::from_secs(60));
assert_eq!(builder.sessions().len(), 1);
assert_eq!(builder.connect_timeout(), Duration::from_secs(60));
}
#[test]
fn test_into_initiator_single_session_carries_connect_timeout() {
let initiator = EngineBuilder::new()
.add_session(session())
.with_connect_timeout(Duration::from_secs(7))
.into_initiator();
match initiator {
Ok(initiator) => {
assert_eq!(initiator.session_id().to_string(), "FIX.4.4:SENDER->TARGET");
}
Err(err) => panic!("a single-session builder must produce an initiator: {err}"),
}
}
#[test]
fn test_into_acceptor_single_session_builds() {
let acceptor = EngineBuilder::new().add_session(session()).into_acceptor();
match acceptor {
Ok(acceptor) => {
assert_eq!(acceptor.session_id().to_string(), "FIX.4.4:SENDER->TARGET");
}
Err(err) => panic!("a single-session builder must produce an acceptor: {err}"),
}
}
#[test]
fn test_into_initiator_without_session_is_configuration_error() {
match EngineBuilder::new().into_initiator() {
Err(EngineError::Configuration(detail)) => assert!(detail.contains("no session")),
other => panic!("an empty builder must fail with Configuration, got {other:?}"),
}
}
#[test]
fn test_into_acceptor_with_two_sessions_is_configuration_error() {
match EngineBuilder::new()
.add_session(session())
.add_session(session())
.into_acceptor()
{
Err(EngineError::Configuration(detail)) => assert!(detail.contains("2 sessions")),
other => panic!("a two-session builder must fail with Configuration, got {other:?}"),
}
}
}