1pub mod env;
82pub mod file_watcher;
83pub mod hot_reload;
84pub mod loader;
85pub mod validation;
86pub mod watcher;
87
88use std::collections::HashMap;
89use std::net::SocketAddr;
90use std::path::PathBuf;
91use std::time::Duration;
92
93use serde::{Deserialize, Serialize};
94
95use crate::protocol::Protocol;
96use crate::tags::Tags;
97
98pub use watcher::{
100 CallbackHandler, ConfigEvent, ConfigEventHandler, ConfigSource, ConfigWatcher,
101 SharedConfigWatcher, WatcherState, create_config_watcher,
102};
103
104pub use loader::{ConfigFormat, ConfigLoader, ConfigDiscovery, LayeredConfigBuilder};
106
107pub use env::{
109 EnvOverrides, EnvRule, EnvRuleBuilder, EnvApplyResult, EnvConfigurable,
110 EnvSnapshot, EnvVarDoc, DEFAULT_PREFIX,
111 get_env, get_env_or, get_env_bool, get_env_bool_or,
112};
113
114pub use validation::{
116 Validatable, Validator, ValidationContext, ValidationRule,
117 RangeRule, StringLengthRule, PathExistsRule, SocketAddrRule,
118 CrossFieldValidator,
119};
120
121pub use file_watcher::{
123 FileWatcherService, FileWatcherServiceBuilder, FileWatcherConfig,
124 DEFAULT_DEBOUNCE_MS,
125};
126
127pub use hot_reload::{
129 HotReloadManager, HotReloadManagerBuilder, ReloadEvent, ReloadStrategy,
130 ConfigChange,
131};
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct EngineConfig {
136 #[serde(default = "default_engine_name")]
138 pub name: String,
139
140 #[serde(default = "default_max_devices")]
142 pub max_devices: usize,
143
144 #[serde(default = "default_max_points")]
146 pub max_points: usize,
147
148 #[serde(default = "default_tick_interval_ms")]
150 pub tick_interval_ms: u64,
151
152 #[serde(default = "default_workers")]
154 pub workers: usize,
155
156 #[serde(default = "default_true")]
158 pub enable_metrics: bool,
159
160 #[serde(default = "default_metrics_interval")]
162 pub metrics_interval_secs: u64,
163
164 #[serde(default = "default_log_level")]
166 pub log_level: String,
167
168 #[serde(default)]
170 pub protocols: HashMap<String, ProtocolConfig>,
171}
172
173fn default_engine_name() -> String {
174 "trap-simulator".to_string()
175}
176
177fn default_max_devices() -> usize {
178 10_000
179}
180
181fn default_max_points() -> usize {
182 1_000_000
183}
184
185fn default_tick_interval_ms() -> u64 {
186 100
187}
188
189fn default_workers() -> usize {
190 num_cpus::get().max(4)
191}
192
193fn default_true() -> bool {
194 true
195}
196
197fn default_metrics_interval() -> u64 {
198 10
199}
200
201fn default_log_level() -> String {
202 "info".to_string()
203}
204
205impl Default for EngineConfig {
206 fn default() -> Self {
207 Self {
208 name: default_engine_name(),
209 max_devices: default_max_devices(),
210 max_points: default_max_points(),
211 tick_interval_ms: default_tick_interval_ms(),
212 workers: default_workers(),
213 enable_metrics: true,
214 metrics_interval_secs: default_metrics_interval(),
215 log_level: default_log_level(),
216 protocols: HashMap::new(),
217 }
218 }
219}
220
221impl EngineConfig {
222 pub fn new() -> Self {
224 Self::default()
225 }
226
227 pub fn from_yaml_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
229 ConfigLoader::load_with_format(path.into(), ConfigFormat::Yaml)
230 }
231
232 pub fn from_json_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
234 ConfigLoader::load_with_format(path.into(), ConfigFormat::Json)
235 }
236
237 pub fn from_toml_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
239 ConfigLoader::load_with_format(path.into(), ConfigFormat::Toml)
240 }
241
242 pub fn from_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
244 ConfigLoader::load(path.into())
245 }
246
247 pub fn tick_interval(&self) -> Duration {
249 Duration::from_millis(self.tick_interval_ms)
250 }
251
252 pub fn with_max_devices(mut self, max: usize) -> Self {
254 self.max_devices = max;
255 self
256 }
257
258 pub fn with_max_points(mut self, max: usize) -> Self {
260 self.max_points = max;
261 self
262 }
263
264 pub fn with_tick_interval(mut self, interval: Duration) -> Self {
266 self.tick_interval_ms = interval.as_millis() as u64;
267 self
268 }
269
270 pub fn with_workers(mut self, workers: usize) -> Self {
272 self.workers = workers;
273 self
274 }
275
276 pub fn with_log_level(mut self, level: impl Into<String>) -> Self {
278 self.log_level = level.into();
279 self
280 }
281
282 pub fn with_metrics(mut self, enable: bool) -> Self {
284 self.enable_metrics = enable;
285 self
286 }
287
288 pub fn with_protocol(mut self, name: impl Into<String>, config: ProtocolConfig) -> Self {
290 self.protocols.insert(name.into(), config);
291 self
292 }
293
294 pub fn apply_env_overrides(&mut self) -> EnvApplyResult {
299 Self::env_overrides().apply(self)
300 }
301
302 pub fn env_overrides() -> EnvOverrides<Self> {
304 EnvOverrides::with_prefix(DEFAULT_PREFIX)
305 .add_rule(
306 EnvRuleBuilder::new("ENGINE_NAME")
307 .field_path("name")
308 .description("Engine instance name")
309 .as_string(|c: &mut Self, v| c.name = v),
310 )
311 .add_rule(
312 EnvRuleBuilder::new("ENGINE_MAX_DEVICES")
313 .field_path("max_devices")
314 .description("Maximum number of devices")
315 .parse_into(|c: &mut Self, v: usize| c.max_devices = v),
316 )
317 .add_rule(
318 EnvRuleBuilder::new("ENGINE_MAX_POINTS")
319 .field_path("max_points")
320 .description("Maximum number of data points")
321 .parse_into(|c: &mut Self, v: usize| c.max_points = v),
322 )
323 .add_rule(
324 EnvRuleBuilder::new("ENGINE_TICK_INTERVAL_MS")
325 .field_path("tick_interval_ms")
326 .description("Tick interval in milliseconds")
327 .parse_into(|c: &mut Self, v: u64| c.tick_interval_ms = v),
328 )
329 .add_rule(
330 EnvRuleBuilder::new("ENGINE_WORKERS")
331 .field_path("workers")
332 .description("Number of worker threads")
333 .parse_into(|c: &mut Self, v: usize| c.workers = v),
334 )
335 .add_rule(
336 EnvRuleBuilder::new("ENGINE_METRICS")
337 .field_path("enable_metrics")
338 .description("Enable metrics collection")
339 .as_bool(|c: &mut Self, v| c.enable_metrics = v),
340 )
341 .add_rule(
342 EnvRuleBuilder::new("ENGINE_METRICS_INTERVAL")
343 .field_path("metrics_interval_secs")
344 .description("Metrics export interval in seconds")
345 .parse_into(|c: &mut Self, v: u64| c.metrics_interval_secs = v),
346 )
347 .add_rule(
348 EnvRuleBuilder::new("LOG_LEVEL")
349 .field_path("log_level")
350 .description("Log level (trace, debug, info, warn, error)")
351 .as_string(|c: &mut Self, v| c.log_level = v),
352 )
353 }
354}
355
356impl EnvConfigurable for EngineConfig {
357 fn env_overrides() -> EnvOverrides<Self> {
358 Self::env_overrides()
359 }
360}
361
362impl Validatable for EngineConfig {
363 fn validate(&self) -> crate::Result<()> {
364 let mut errors = crate::error::ValidationErrors::new();
365 self.validate_collect(&mut errors);
366 errors.into_result(())
367 }
368
369 fn validate_collect(&self, errors: &mut crate::error::ValidationErrors) {
370 if self.name.trim().is_empty() {
372 errors.add("name", "Engine name cannot be empty");
373 }
374
375 if self.max_devices == 0 {
377 errors.add("max_devices", "Max devices must be greater than 0");
378 }
379 if self.max_devices > 1_000_000 {
380 errors.add("max_devices", "Max devices cannot exceed 1,000,000");
381 }
382
383 if self.max_points == 0 {
385 errors.add("max_points", "Max points must be greater than 0");
386 }
387 if self.max_points > 100_000_000 {
388 errors.add("max_points", "Max points cannot exceed 100,000,000");
389 }
390
391 if self.tick_interval_ms < 1 {
393 errors.add("tick_interval_ms", "Tick interval must be at least 1ms");
394 }
395 if self.tick_interval_ms > 60_000 {
396 errors.add("tick_interval_ms", "Tick interval cannot exceed 60 seconds");
397 }
398
399 if self.workers == 0 {
401 errors.add("workers", "Workers must be greater than 0");
402 }
403 if self.workers > 1024 {
404 errors.add("workers", "Workers cannot exceed 1024");
405 }
406
407 if self.enable_metrics && self.metrics_interval_secs == 0 {
409 errors.add("metrics_interval_secs", "Metrics interval must be greater than 0 when metrics are enabled");
410 }
411
412 let valid_levels = ["trace", "debug", "info", "warn", "error"];
414 if !valid_levels.contains(&self.log_level.to_lowercase().as_str()) {
415 errors.add(
416 "log_level",
417 format!(
418 "Invalid log level '{}', must be one of: {:?}",
419 self.log_level, valid_levels
420 ),
421 );
422 }
423
424 let points_per_device = self.max_points / self.max_devices.max(1);
426 if points_per_device > 10_000 {
427 errors.add(
428 "max_points, max_devices",
429 format!(
430 "Average points per device ({}) seems too high",
431 points_per_device
432 ),
433 );
434 }
435 }
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
440#[serde(tag = "type", rename_all = "lowercase")]
441pub enum ProtocolConfig {
442 ModbusTcp(ModbusTcpConfig),
444 ModbusRtu(ModbusRtuConfig),
446 OpcUa(OpcUaConfig),
448 BacnetIp(BacnetIpConfig),
450 KnxIp(KnxIpConfig),
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize)]
456pub struct ModbusTcpConfig {
457 #[serde(default = "default_modbus_bind")]
459 pub bind_address: SocketAddr,
460
461 #[serde(default = "default_max_connections")]
463 pub max_connections: usize,
464
465 #[serde(default = "default_timeout")]
467 pub timeout_secs: u64,
468
469 #[serde(default = "default_true")]
471 pub keep_alive: bool,
472}
473
474fn default_modbus_bind() -> SocketAddr {
475 "0.0.0.0:502".parse().unwrap()
476}
477
478fn default_max_connections() -> usize {
479 1000
480}
481
482fn default_timeout() -> u64 {
483 30
484}
485
486impl Default for ModbusTcpConfig {
487 fn default() -> Self {
488 Self {
489 bind_address: default_modbus_bind(),
490 max_connections: default_max_connections(),
491 timeout_secs: default_timeout(),
492 keep_alive: true,
493 }
494 }
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct ModbusRtuConfig {
500 pub serial_port: String,
502
503 #[serde(default = "default_baud_rate")]
505 pub baud_rate: u32,
506
507 #[serde(default = "default_data_bits")]
509 pub data_bits: u8,
510
511 #[serde(default = "default_parity")]
513 pub parity: String,
514
515 #[serde(default = "default_stop_bits")]
517 pub stop_bits: u8,
518}
519
520fn default_baud_rate() -> u32 {
521 9600
522}
523
524fn default_data_bits() -> u8 {
525 8
526}
527
528fn default_parity() -> String {
529 "none".to_string()
530}
531
532fn default_stop_bits() -> u8 {
533 1
534}
535
536impl Default for ModbusRtuConfig {
537 fn default() -> Self {
538 Self {
539 serial_port: "/dev/ttyUSB0".to_string(),
540 baud_rate: default_baud_rate(),
541 data_bits: default_data_bits(),
542 parity: default_parity(),
543 stop_bits: default_stop_bits(),
544 }
545 }
546}
547
548#[derive(Debug, Clone, Serialize, Deserialize)]
550pub struct OpcUaConfig {
551 #[serde(default = "default_opcua_endpoint")]
553 pub endpoint_url: String,
554
555 #[serde(default = "default_opcua_server_name")]
557 pub server_name: String,
558
559 #[serde(default = "default_security_policy")]
561 pub security_policy: String,
562
563 pub certificate_path: Option<PathBuf>,
565
566 pub private_key_path: Option<PathBuf>,
568
569 #[serde(default = "default_max_subscriptions")]
571 pub max_subscriptions: usize,
572}
573
574fn default_opcua_endpoint() -> String {
575 "opc.tcp://0.0.0.0:4840".to_string()
576}
577
578fn default_opcua_server_name() -> String {
579 "TRAP Simulator OPC UA Server".to_string()
580}
581
582fn default_security_policy() -> String {
583 "None".to_string()
584}
585
586fn default_max_subscriptions() -> usize {
587 100
588}
589
590impl Default for OpcUaConfig {
591 fn default() -> Self {
592 Self {
593 endpoint_url: default_opcua_endpoint(),
594 server_name: default_opcua_server_name(),
595 security_policy: default_security_policy(),
596 certificate_path: None,
597 private_key_path: None,
598 max_subscriptions: default_max_subscriptions(),
599 }
600 }
601}
602
603#[derive(Debug, Clone, Serialize, Deserialize)]
605pub struct BacnetIpConfig {
606 #[serde(default = "default_bacnet_bind")]
608 pub bind_address: SocketAddr,
609
610 #[serde(default = "default_device_instance")]
612 pub device_instance: u32,
613
614 #[serde(default = "default_bacnet_device_name")]
616 pub device_name: String,
617
618 #[serde(default)]
620 pub enable_bbmd: bool,
621
622 #[serde(default)]
624 pub bbmd_table: Vec<String>,
625}
626
627fn default_bacnet_bind() -> SocketAddr {
628 "0.0.0.0:47808".parse().unwrap()
629}
630
631fn default_device_instance() -> u32 {
632 1234
633}
634
635fn default_bacnet_device_name() -> String {
636 "TRAP Simulator BACnet Device".to_string()
637}
638
639impl Default for BacnetIpConfig {
640 fn default() -> Self {
641 Self {
642 bind_address: default_bacnet_bind(),
643 device_instance: default_device_instance(),
644 device_name: default_bacnet_device_name(),
645 enable_bbmd: false,
646 bbmd_table: Vec::new(),
647 }
648 }
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct KnxIpConfig {
654 #[serde(default = "default_knx_bind")]
656 pub bind_address: SocketAddr,
657
658 #[serde(default = "default_individual_address")]
660 pub individual_address: String,
661
662 #[serde(default = "default_true")]
664 pub enable_tunneling: bool,
665
666 #[serde(default)]
668 pub enable_routing: bool,
669
670 #[serde(default = "default_multicast_address")]
672 pub multicast_address: String,
673}
674
675fn default_knx_bind() -> SocketAddr {
676 "0.0.0.0:3671".parse().unwrap()
677}
678
679fn default_individual_address() -> String {
680 "1.1.1".to_string()
681}
682
683fn default_multicast_address() -> String {
684 "224.0.23.12".to_string()
685}
686
687impl Default for KnxIpConfig {
688 fn default() -> Self {
689 Self {
690 bind_address: default_knx_bind(),
691 individual_address: default_individual_address(),
692 enable_tunneling: true,
693 enable_routing: false,
694 multicast_address: default_multicast_address(),
695 }
696 }
697}
698
699#[derive(Debug, Clone, Serialize, Deserialize)]
701pub struct DeviceConfig {
702 pub id: String,
704
705 pub name: String,
707
708 #[serde(default)]
710 pub description: String,
711
712 pub protocol: Protocol,
714
715 #[serde(default)]
717 pub address: Option<String>,
718
719 #[serde(default)]
721 pub points: Vec<DataPointConfig>,
722
723 #[serde(default)]
725 pub metadata: HashMap<String, String>,
726
727 #[serde(default, skip_serializing_if = "Tags::is_empty")]
729 pub tags: Tags,
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize)]
734pub struct DataPointConfig {
735 pub id: String,
737
738 pub name: String,
740
741 pub data_type: String,
743
744 #[serde(default = "default_access")]
746 pub access: String,
747
748 #[serde(default)]
750 pub address: Option<String>,
751
752 #[serde(default)]
754 pub initial_value: Option<serde_json::Value>,
755
756 #[serde(default)]
758 pub units: Option<String>,
759
760 #[serde(default)]
762 pub min: Option<f64>,
763
764 #[serde(default)]
766 pub max: Option<f64>,
767}
768
769fn default_access() -> String {
770 "rw".to_string()
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776 use std::env;
777
778 #[test]
779 fn test_engine_config_default() {
780 let config = EngineConfig::default();
781 assert_eq!(config.max_devices, 10_000);
782 assert_eq!(config.tick_interval_ms, 100);
783 assert_eq!(config.name, "trap-simulator");
784 }
785
786 #[test]
787 fn test_engine_config_builder() {
788 let config = EngineConfig::new()
789 .with_max_devices(50_000)
790 .with_max_points(5_000_000)
791 .with_tick_interval(Duration::from_millis(50))
792 .with_workers(8)
793 .with_log_level("debug")
794 .with_metrics(false);
795
796 assert_eq!(config.max_devices, 50_000);
797 assert_eq!(config.max_points, 5_000_000);
798 assert_eq!(config.tick_interval_ms, 50);
799 assert_eq!(config.workers, 8);
800 assert_eq!(config.log_level, "debug");
801 assert!(!config.enable_metrics);
802 }
803
804 #[test]
805 fn test_modbus_tcp_config_default() {
806 let config = ModbusTcpConfig::default();
807 assert_eq!(config.bind_address.port(), 502);
808 assert_eq!(config.max_connections, 1000);
809 }
810
811 #[test]
812 fn test_config_serialization_yaml() {
813 let config = EngineConfig::default();
814 let yaml = ConfigLoader::serialize(&config, ConfigFormat::Yaml).unwrap();
815 let parsed: EngineConfig = ConfigLoader::parse(&yaml, ConfigFormat::Yaml).unwrap();
816 assert_eq!(config.max_devices, parsed.max_devices);
817 assert_eq!(config.name, parsed.name);
818 }
819
820 #[test]
821 fn test_config_serialization_json() {
822 let config = EngineConfig::default();
823 let json = ConfigLoader::serialize(&config, ConfigFormat::Json).unwrap();
824 let parsed: EngineConfig = ConfigLoader::parse(&json, ConfigFormat::Json).unwrap();
825 assert_eq!(config.max_devices, parsed.max_devices);
826 }
827
828 #[test]
829 fn test_config_serialization_toml() {
830 let config = EngineConfig::default();
831 let toml = ConfigLoader::serialize(&config, ConfigFormat::Toml).unwrap();
832 let parsed: EngineConfig = ConfigLoader::parse(&toml, ConfigFormat::Toml).unwrap();
833 assert_eq!(config.max_devices, parsed.max_devices);
834 }
835
836 #[test]
837 fn test_config_validation_valid() {
838 let config = EngineConfig::default();
839 assert!(config.validate().is_ok());
840 }
841
842 #[test]
843 fn test_config_validation_invalid_max_devices() {
844 let config = EngineConfig::default().with_max_devices(0);
845 let result = config.validate();
846 assert!(result.is_err());
847 }
848
849 #[test]
850 fn test_config_validation_invalid_log_level() {
851 let mut config = EngineConfig::default();
852 config.log_level = "invalid".to_string();
853 let result = config.validate();
854 assert!(result.is_err());
855 }
856
857 #[test]
858 fn test_config_validation_cross_field() {
859 let config = EngineConfig::default()
861 .with_max_devices(10)
862 .with_max_points(1_000_000);
863 let result = config.validate();
864 assert!(result.is_err());
865 }
866
867 #[test]
868 fn test_env_overrides() {
869 env::set_var("TRAP_SIM_ENGINE_MAX_DEVICES", "25000");
871 env::set_var("TRAP_SIM_ENGINE_WORKERS", "16");
872 env::set_var("TRAP_SIM_LOG_LEVEL", "debug");
873
874 let mut config = EngineConfig::default();
875 let result = config.apply_env_overrides();
876
877 assert!(result.has_changes());
878 assert_eq!(config.max_devices, 25000);
879 assert_eq!(config.workers, 16);
880 assert_eq!(config.log_level, "debug");
881
882 env::remove_var("TRAP_SIM_ENGINE_MAX_DEVICES");
884 env::remove_var("TRAP_SIM_ENGINE_WORKERS");
885 env::remove_var("TRAP_SIM_LOG_LEVEL");
886 }
887
888 #[test]
889 fn test_env_overrides_documentation() {
890 let overrides = EngineConfig::env_overrides();
891 let docs = overrides.documentation();
892
893 assert!(docs.len() > 0);
894 assert!(docs.iter().any(|d| d.var_name == "TRAP_SIM_ENGINE_MAX_DEVICES"));
895 assert!(docs.iter().any(|d| d.var_name == "TRAP_SIM_LOG_LEVEL"));
896 }
897
898 #[test]
899 fn test_protocol_config_modbus_tcp() {
900 let config = ProtocolConfig::ModbusTcp(ModbusTcpConfig::default());
901 let yaml = serde_yaml::to_string(&config).unwrap();
902 assert!(yaml.contains("type: modbustcp"));
903 }
904
905 #[test]
906 fn test_protocol_config_opcua() {
907 let config = ProtocolConfig::OpcUa(OpcUaConfig::default());
908 let yaml = serde_yaml::to_string(&config).unwrap();
909 assert!(yaml.contains("type: opcua"));
910 }
911
912 #[test]
913 fn test_protocol_config_bacnet() {
914 let config = ProtocolConfig::BacnetIp(BacnetIpConfig::default());
915 assert_eq!(BacnetIpConfig::default().device_instance, 1234);
916 }
917
918 #[test]
919 fn test_protocol_config_knx() {
920 let config = ProtocolConfig::KnxIp(KnxIpConfig::default());
921 assert_eq!(KnxIpConfig::default().individual_address, "1.1.1");
922 }
923
924 #[test]
925 fn test_engine_config_with_protocol() {
926 let config = EngineConfig::default()
927 .with_protocol("modbus", ProtocolConfig::ModbusTcp(ModbusTcpConfig::default()));
928
929 assert!(config.protocols.contains_key("modbus"));
930 }
931
932 #[test]
933 fn test_config_format_detection() {
934 assert_eq!(ConfigFormat::from_path("config.yaml"), Some(ConfigFormat::Yaml));
935 assert_eq!(ConfigFormat::from_path("config.yml"), Some(ConfigFormat::Yaml));
936 assert_eq!(ConfigFormat::from_path("config.json"), Some(ConfigFormat::Json));
937 assert_eq!(ConfigFormat::from_path("config.toml"), Some(ConfigFormat::Toml));
938 assert_eq!(ConfigFormat::from_path("config.txt"), None);
939 }
940}