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