1use serde::{Deserialize, Serialize};
17use ustr::Ustr;
18
19use super::{BusPayloadCategory, BusPayloadType};
20use crate::{
21 config::{ConfigError, ConfigErrorCollector, ConfigResult},
22 enums::SerializationEncoding,
23};
24
25#[cfg_attr(
27 feature = "python",
28 pyo3::pyclass(module = "nautilus_trader.common", from_py_object)
29)]
30#[cfg_attr(
31 feature = "python",
32 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.common")
33)]
34#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, bon::Builder)]
35#[serde(default, deny_unknown_fields)]
36pub struct MessageBusConfig {
37 #[builder(default = SerializationEncoding::Json)]
39 pub encoding: SerializationEncoding,
40 pub encoding_market_data: Option<SerializationEncoding>,
42 pub encoding_builtin: Option<SerializationEncoding>,
44 #[builder(default)]
47 pub timestamps_as_iso8601: bool,
48 pub buffer_interval_ms: Option<u32>,
52 pub autotrim_mins: Option<u32>,
56 pub autotrim_maxlen: Option<u32>,
59 #[builder(default = true)]
61 pub use_trader_prefix: bool,
62 #[builder(default = true)]
64 pub use_trader_id: bool,
65 #[builder(default)]
67 pub use_instance_id: bool,
68 #[builder(default = "stream".to_string())]
70 pub streams_prefix: String,
71 #[builder(default = true)]
74 pub stream_per_topic: bool,
75 pub external_streams: Option<Vec<String>>,
77 pub types_filter: Option<Vec<String>>,
79 pub heartbeat_interval_secs: Option<u16>,
81}
82
83impl Default for MessageBusConfig {
84 fn default() -> Self {
85 Self::builder().build()
86 }
87}
88
89impl MessageBusConfig {
90 pub fn validate(&self) -> ConfigResult<()> {
97 let mut errors = ConfigErrorCollector::new();
98
99 if !BusPayloadType::Custom(Ustr::from("Custom")).supports(self.encoding) {
100 errors.push(ConfigError::unsupported_value(
101 "MessageBusConfig.encoding",
102 format!(
103 "{} does not support custom or unmapped payloads",
104 self.encoding
105 ),
106 ));
107 }
108
109 if let Some(encoding) = self.encoding_market_data {
110 validate_category_encoding(
111 &mut errors,
112 "MessageBusConfig.encoding_market_data",
113 BusPayloadCategory::MarketData,
114 encoding,
115 );
116 }
117
118 if let Some(encoding) = self.encoding_builtin {
119 validate_category_encoding(
120 &mut errors,
121 "MessageBusConfig.encoding_builtin",
122 BusPayloadCategory::BuiltIn,
123 encoding,
124 );
125 }
126
127 errors.into_result()
128 }
129}
130
131fn validate_category_encoding(
132 errors: &mut ConfigErrorCollector,
133 field: &'static str,
134 category: BusPayloadCategory,
135 encoding: SerializationEncoding,
136) {
137 let unsupported = BusPayloadType::PUBLISHED_TYPES
138 .iter()
139 .copied()
140 .filter(|payload_type| payload_type.category() == category)
141 .filter(|payload_type| !payload_type.supports(encoding))
142 .map(|payload_type| payload_type.as_str().to_string())
143 .collect::<Vec<_>>();
144
145 if unsupported.is_empty() {
146 return;
147 }
148
149 errors.push(ConfigError::unsupported_value(
150 field,
151 format!(
152 "{} is not supported by {}",
153 encoding,
154 unsupported.join(", ")
155 ),
156 ));
157}
158
159#[cfg(test)]
160mod tests {
161 use rstest::*;
162 use serde_json::json;
163
164 use super::*;
165 use crate::{config::ConfigError, enums::SerializationEncoding};
166
167 #[rstest]
168 fn test_default_message_bus_config() {
169 let config = MessageBusConfig::default();
170 assert_eq!(config.encoding, SerializationEncoding::Json);
171 assert_eq!(config.encoding_market_data, None);
172 assert_eq!(config.encoding_builtin, None);
173 assert!(!config.timestamps_as_iso8601);
174 assert_eq!(config.buffer_interval_ms, None);
175 assert_eq!(config.autotrim_mins, None);
176 assert_eq!(config.autotrim_maxlen, None);
177 assert!(config.use_trader_prefix);
178 assert!(config.use_trader_id);
179 assert!(!config.use_instance_id);
180 assert_eq!(config.streams_prefix, "stream");
181 assert!(config.stream_per_topic);
182 assert_eq!(config.external_streams, None);
183 assert_eq!(config.types_filter, None);
184 }
185
186 #[rstest]
187 fn test_deserialize_message_bus_config() {
188 let config_json = json!({
189 "encoding": "json",
190 "encoding_market_data": "sbe",
191 "encoding_builtin": "msgpack",
192 "timestamps_as_iso8601": true,
193 "buffer_interval_ms": 100,
194 "autotrim_mins": 60,
195 "autotrim_maxlen": 10_000,
196 "use_trader_prefix": false,
197 "use_trader_id": false,
198 "use_instance_id": true,
199 "streams_prefix": "data_streams",
200 "stream_per_topic": false,
201 "external_streams": ["stream1", "stream2"],
202 "types_filter": ["type1", "type2"]
203 });
204 let config: MessageBusConfig = serde_json::from_value(config_json).unwrap();
205 assert_eq!(config.encoding, SerializationEncoding::Json);
206 assert_eq!(
207 config.encoding_market_data,
208 Some(SerializationEncoding::Sbe)
209 );
210 assert_eq!(
211 config.encoding_builtin,
212 Some(SerializationEncoding::MsgPack)
213 );
214 assert!(config.timestamps_as_iso8601);
215 assert_eq!(config.buffer_interval_ms, Some(100));
216 assert_eq!(config.autotrim_mins, Some(60));
217 assert_eq!(config.autotrim_maxlen, Some(10_000));
218 assert!(!config.use_trader_prefix);
219 assert!(!config.use_trader_id);
220 assert!(config.use_instance_id);
221 assert_eq!(config.streams_prefix, "data_streams");
222 assert!(!config.stream_per_topic);
223 assert_eq!(
224 config.external_streams,
225 Some(vec!["stream1".to_string(), "stream2".to_string()])
226 );
227 assert_eq!(
228 config.types_filter,
229 Some(vec!["type1".to_string(), "type2".to_string()])
230 );
231 }
232
233 #[rstest]
234 fn test_deserialize_message_bus_config_rejects_backing_field() {
235 let config_json = json!({
236 "backing": {},
237 });
238
239 let error = serde_json::from_value::<MessageBusConfig>(config_json).unwrap_err();
240 assert!(error.to_string().contains("unknown field `backing`"));
241 }
242
243 #[rstest]
244 #[case("sbe", SerializationEncoding::Sbe)]
245 #[case("capnp", SerializationEncoding::Capnp)]
246 fn test_deserialize_message_bus_config_with_schema_encoding(
247 #[case] encoding_name: &str,
248 #[case] expected: SerializationEncoding,
249 ) {
250 let config_json = json!({
251 "encoding": encoding_name,
252 });
253
254 let config: MessageBusConfig = serde_json::from_value(config_json).unwrap();
255 assert_eq!(config.encoding, expected);
256 }
257
258 #[rstest]
259 fn message_bus_config_validate_accepts_default() {
260 let config = MessageBusConfig::default();
261
262 assert!(config.validate().is_ok());
263 }
264
265 #[rstest]
266 #[case(SerializationEncoding::Json)]
267 #[case(SerializationEncoding::MsgPack)]
268 fn message_bus_config_validate_accepts_custom_safe_default(
269 #[case] encoding: SerializationEncoding,
270 ) {
271 let config = MessageBusConfig {
272 encoding,
273 ..Default::default()
274 };
275
276 assert!(config.validate().is_ok());
277 }
278
279 #[rstest]
280 #[case(SerializationEncoding::Sbe)]
281 #[case(SerializationEncoding::Capnp)]
282 fn message_bus_config_validate_rejects_schema_default(#[case] encoding: SerializationEncoding) {
283 let config = MessageBusConfig {
284 encoding,
285 ..Default::default()
286 };
287
288 let error = config.validate().unwrap_err();
289 assert!(matches!(
290 error,
291 ConfigError::UnsupportedValue { field, .. }
292 if field == "MessageBusConfig.encoding"
293 ));
294 }
295
296 #[cfg(any(feature = "sbe", feature = "capnp"))]
297 #[rstest]
298 #[cfg_attr(feature = "sbe", case(SerializationEncoding::Sbe))]
299 #[cfg_attr(feature = "capnp", case(SerializationEncoding::Capnp))]
300 fn message_bus_config_validate_accepts_market_data_override(
301 #[case] encoding: SerializationEncoding,
302 ) {
303 let config = MessageBusConfig {
304 encoding_market_data: Some(encoding),
305 ..Default::default()
306 };
307
308 assert!(config.validate().is_ok());
309 }
310
311 #[cfg(not(feature = "sbe"))]
312 #[rstest]
313 fn message_bus_config_validate_rejects_market_data_sbe_without_feature() {
314 let config = MessageBusConfig {
315 encoding_market_data: Some(SerializationEncoding::Sbe),
316 ..Default::default()
317 };
318
319 let error = config.validate().unwrap_err();
320 assert!(matches!(
321 error,
322 ConfigError::UnsupportedValue { field, .. }
323 if field == "MessageBusConfig.encoding_market_data"
324 ));
325 }
326
327 #[cfg(not(feature = "capnp"))]
328 #[rstest]
329 fn message_bus_config_validate_rejects_market_data_capnp_without_feature() {
330 let config = MessageBusConfig {
331 encoding_market_data: Some(SerializationEncoding::Capnp),
332 ..Default::default()
333 };
334
335 let error = config.validate().unwrap_err();
336 assert!(matches!(
337 error,
338 ConfigError::UnsupportedValue { field, .. }
339 if field == "MessageBusConfig.encoding_market_data"
340 ));
341 }
342
343 #[rstest]
344 #[case(SerializationEncoding::Sbe)]
345 #[case(SerializationEncoding::Capnp)]
346 fn message_bus_config_validate_rejects_builtin_schema_override(
347 #[case] encoding: SerializationEncoding,
348 ) {
349 let config = MessageBusConfig {
350 encoding_builtin: Some(encoding),
351 ..Default::default()
352 };
353
354 let error = config.validate().unwrap_err();
355 assert!(matches!(
356 error,
357 ConfigError::UnsupportedValue { field, .. }
358 if field == "MessageBusConfig.encoding_builtin"
359 ));
360 }
361}