ironfix_engine/
builder.rs1use crate::acceptor::Acceptor;
22use crate::application::{Application, NoOpApplication};
23use crate::error::EngineError;
24use crate::initiator::Initiator;
25use ironfix_session::config::SessionConfig;
26use std::sync::Arc;
27use std::time::Duration;
28
29#[derive(Debug)]
35pub struct EngineBuilder<A: Application = NoOpApplication> {
36 application: Arc<A>,
38 sessions: Vec<SessionConfig>,
40 connect_timeout: Duration,
42}
43
44impl Default for EngineBuilder<NoOpApplication> {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl EngineBuilder<NoOpApplication> {
51 #[must_use]
53 pub fn new() -> Self {
54 Self {
55 application: Arc::new(NoOpApplication),
56 sessions: Vec::new(),
57 connect_timeout: Duration::from_secs(30),
58 }
59 }
60}
61
62impl<A: Application + 'static> EngineBuilder<A> {
63 #[must_use]
65 pub fn with_application<B: Application>(self, application: B) -> EngineBuilder<B> {
66 EngineBuilder {
67 application: Arc::new(application),
68 sessions: self.sessions,
69 connect_timeout: self.connect_timeout,
70 }
71 }
72
73 #[must_use]
79 pub fn add_session(mut self, config: SessionConfig) -> Self {
80 self.sessions.push(config);
81 self
82 }
83
84 #[must_use]
87 pub fn with_connect_timeout(mut self, timeout: Duration) -> Self {
88 self.connect_timeout = timeout;
89 self
90 }
91
92 #[must_use]
94 pub fn sessions(&self) -> &[SessionConfig] {
95 &self.sessions
96 }
97
98 #[must_use]
100 pub const fn connect_timeout(&self) -> Duration {
101 self.connect_timeout
102 }
103
104 #[must_use]
106 pub fn application(&self) -> Arc<A> {
107 Arc::clone(&self.application)
108 }
109
110 pub fn into_initiator(self) -> Result<Initiator<A>, EngineError> {
117 let config = self.single_session()?;
118 Ok(Initiator::new(config, self.application).with_connect_timeout(self.connect_timeout))
119 }
120
121 pub fn into_acceptor(self) -> Result<Acceptor<A>, EngineError> {
128 let config = self.single_session()?;
129 Ok(Acceptor::new(config, self.application))
130 }
131
132 fn single_session(&self) -> Result<SessionConfig, EngineError> {
135 match self.sessions.as_slice() {
136 [config] => Ok(config.clone()),
137 [] => Err(EngineError::Configuration(
138 "no session configured: add exactly one with add_session".to_string(),
139 )),
140 many => Err(EngineError::Configuration(format!(
141 "{} sessions configured, but a single-session engine requires exactly one",
142 many.len()
143 ))),
144 }
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use ironfix_core::types::CompId;
152
153 #[track_caller]
155 fn comp_id(value: &str) -> CompId {
156 match CompId::new(value) {
157 Ok(id) => id,
158 Err(err) => panic!("test CompId must be valid: {err}"),
159 }
160 }
161
162 fn session() -> SessionConfig {
163 SessionConfig::new(comp_id("SENDER"), comp_id("TARGET"), "FIX.4.4")
164 }
165
166 #[test]
167 fn test_engine_builder_default_is_empty() {
168 let builder = EngineBuilder::new();
169 assert_eq!(builder.connect_timeout(), Duration::from_secs(30));
170 assert!(builder.sessions().is_empty());
171 }
172
173 #[test]
174 fn test_engine_builder_add_session_records_it() {
175 let builder = EngineBuilder::new()
176 .add_session(session())
177 .with_connect_timeout(Duration::from_secs(60));
178
179 assert_eq!(builder.sessions().len(), 1);
180 assert_eq!(builder.connect_timeout(), Duration::from_secs(60));
181 }
182
183 #[test]
184 fn test_into_initiator_single_session_carries_connect_timeout() {
185 let initiator = EngineBuilder::new()
186 .add_session(session())
187 .with_connect_timeout(Duration::from_secs(7))
188 .into_initiator();
189
190 match initiator {
191 Ok(initiator) => {
192 assert_eq!(initiator.session_id().to_string(), "FIX.4.4:SENDER->TARGET");
193 }
194 Err(err) => panic!("a single-session builder must produce an initiator: {err}"),
195 }
196 }
197
198 #[test]
199 fn test_into_acceptor_single_session_builds() {
200 let acceptor = EngineBuilder::new().add_session(session()).into_acceptor();
201
202 match acceptor {
203 Ok(acceptor) => {
204 assert_eq!(acceptor.session_id().to_string(), "FIX.4.4:SENDER->TARGET");
205 }
206 Err(err) => panic!("a single-session builder must produce an acceptor: {err}"),
207 }
208 }
209
210 #[test]
211 fn test_into_initiator_without_session_is_configuration_error() {
212 match EngineBuilder::new().into_initiator() {
213 Err(EngineError::Configuration(detail)) => assert!(detail.contains("no session")),
214 other => panic!("an empty builder must fail with Configuration, got {other:?}"),
215 }
216 }
217
218 #[test]
219 fn test_into_acceptor_with_two_sessions_is_configuration_error() {
220 match EngineBuilder::new()
221 .add_session(session())
222 .add_session(session())
223 .into_acceptor()
224 {
225 Err(EngineError::Configuration(detail)) => assert!(detail.contains("2 sessions")),
226 other => panic!("a two-session builder must fail with Configuration, got {other:?}"),
227 }
228 }
229}