1pub mod env;
83pub mod file_watcher;
84pub mod hot_reload;
85pub mod loader;
86pub mod validation;
87pub mod watcher;
88
89use std::collections::HashMap;
90use std::net::SocketAddr;
91use std::path::PathBuf;
92use std::time::Duration;
93
94use serde::{Deserialize, Serialize};
95
96use crate::protocol::Protocol;
97use crate::tags::Tags;
98
99pub use watcher::{
101 create_config_watcher, CallbackHandler, ConfigEvent, ConfigEventHandler, ConfigSource,
102 ConfigWatcher, SharedConfigWatcher, WatcherState,
103};
104
105pub use loader::{ConfigDiscovery, ConfigFormat, ConfigLoader, LayeredConfigBuilder};
107
108pub use env::{
110 get_env, get_env_bool, get_env_bool_or, get_env_or, EnvApplyResult, EnvConfigurable,
111 EnvOverrides, EnvRule, EnvRuleBuilder, EnvSnapshot, EnvVarDoc, DEFAULT_PREFIX,
112};
113
114pub use validation::{
116 CrossFieldValidator, PathExistsRule, RangeRule, SocketAddrRule, StringLengthRule, Validatable,
117 ValidationContext, ValidationRule, Validator,
118};
119
120pub use file_watcher::{
122 FileWatcherConfig, FileWatcherService, FileWatcherServiceBuilder, DEFAULT_DEBOUNCE_MS,
123};
124
125pub use hot_reload::{
127 ConfigChange, HotReloadManager, HotReloadManagerBuilder, ReloadEvent, ReloadStrategy,
128};
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct EngineConfig {
133 #[serde(default = "default_engine_name")]
135 pub name: String,
136
137 #[serde(default = "default_max_devices")]
139 pub max_devices: usize,
140
141 #[serde(default = "default_max_points")]
143 pub max_points: usize,
144
145 #[serde(default = "default_tick_interval_ms")]
147 pub tick_interval_ms: u64,
148
149 #[serde(default = "default_workers")]
151 pub workers: usize,
152
153 #[serde(default = "default_true")]
155 pub enable_metrics: bool,
156
157 #[serde(default = "default_metrics_interval")]
159 pub metrics_interval_secs: u64,
160
161 #[serde(default = "default_log_level")]
163 pub log_level: String,
164
165 #[serde(default)]
167 pub protocols: HashMap<String, ProtocolConfig>,
168}
169
170fn default_engine_name() -> String {
171 "trap-simulator".to_string()
172}
173
174fn default_max_devices() -> usize {
175 10_000
176}
177
178fn default_max_points() -> usize {
179 1_000_000
180}
181
182fn default_tick_interval_ms() -> u64 {
183 100
184}
185
186fn default_workers() -> usize {
187 num_cpus::get().max(4)
188}
189
190fn default_true() -> bool {
191 true
192}
193
194fn default_metrics_interval() -> u64 {
195 10
196}
197
198fn default_log_level() -> String {
199 "info".to_string()
200}
201
202impl Default for EngineConfig {
203 fn default() -> Self {
204 Self {
205 name: default_engine_name(),
206 max_devices: default_max_devices(),
207 max_points: default_max_points(),
208 tick_interval_ms: default_tick_interval_ms(),
209 workers: default_workers(),
210 enable_metrics: true,
211 metrics_interval_secs: default_metrics_interval(),
212 log_level: default_log_level(),
213 protocols: HashMap::new(),
214 }
215 }
216}
217
218impl EngineConfig {
219 pub fn new() -> Self {
221 Self::default()
222 }
223
224 pub fn from_yaml_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
226 ConfigLoader::load_with_format(path.into(), ConfigFormat::Yaml)
227 }
228
229 pub fn from_json_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
231 ConfigLoader::load_with_format(path.into(), ConfigFormat::Json)
232 }
233
234 pub fn from_toml_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
236 ConfigLoader::load_with_format(path.into(), ConfigFormat::Toml)
237 }
238
239 pub fn from_file(path: impl Into<PathBuf>) -> crate::Result<Self> {
241 ConfigLoader::load(path.into())
242 }
243
244 pub fn tick_interval(&self) -> Duration {
246 Duration::from_millis(self.tick_interval_ms)
247 }
248
249 pub fn with_max_devices(mut self, max: usize) -> Self {
251 self.max_devices = max;
252 self
253 }
254
255 pub fn with_max_points(mut self, max: usize) -> Self {
257 self.max_points = max;
258 self
259 }
260
261 pub fn with_tick_interval(mut self, interval: Duration) -> Self {
263 self.tick_interval_ms = interval.as_millis() as u64;
264 self
265 }
266
267 pub fn with_workers(mut self, workers: usize) -> Self {
269 self.workers = workers;
270 self
271 }
272
273 pub fn with_log_level(mut self, level: impl Into<String>) -> Self {
275 self.log_level = level.into();
276 self
277 }
278
279 pub fn with_metrics(mut self, enable: bool) -> Self {
281 self.enable_metrics = enable;
282 self
283 }
284
285 pub fn with_protocol(mut self, name: impl Into<String>, config: ProtocolConfig) -> Self {
287 self.protocols.insert(name.into(), config);
288 self
289 }
290
291 pub fn apply_env_overrides(&mut self) -> EnvApplyResult {
296 Self::env_overrides().apply(self)
297 }
298
299 pub fn env_overrides() -> EnvOverrides<Self> {
301 EnvOverrides::with_prefix(DEFAULT_PREFIX)
302 .add_rule(
303 EnvRuleBuilder::new("ENGINE_NAME")
304 .field_path("name")
305 .description("Engine instance name")
306 .as_string(|c: &mut Self, v| c.name = v),
307 )
308 .add_rule(
309 EnvRuleBuilder::new("ENGINE_MAX_DEVICES")
310 .field_path("max_devices")
311 .description("Maximum number of devices")
312 .parse_into(|c: &mut Self, v: usize| c.max_devices = v),
313 )
314 .add_rule(
315 EnvRuleBuilder::new("ENGINE_MAX_POINTS")
316 .field_path("max_points")
317 .description("Maximum number of data points")
318 .parse_into(|c: &mut Self, v: usize| c.max_points = v),
319 )
320 .add_rule(
321 EnvRuleBuilder::new("ENGINE_TICK_INTERVAL_MS")
322 .field_path("tick_interval_ms")
323 .description("Tick interval in milliseconds")
324 .parse_into(|c: &mut Self, v: u64| c.tick_interval_ms = v),
325 )
326 .add_rule(
327 EnvRuleBuilder::new("ENGINE_WORKERS")
328 .field_path("workers")
329 .description("Number of worker threads")
330 .parse_into(|c: &mut Self, v: usize| c.workers = v),
331 )
332 .add_rule(
333 EnvRuleBuilder::new("ENGINE_METRICS")
334 .field_path("enable_metrics")
335 .description("Enable metrics collection")
336 .as_bool(|c: &mut Self, v| c.enable_metrics = v),
337 )
338 .add_rule(
339 EnvRuleBuilder::new("ENGINE_METRICS_INTERVAL")
340 .field_path("metrics_interval_secs")
341 .description("Metrics export interval in seconds")
342 .parse_into(|c: &mut Self, v: u64| c.metrics_interval_secs = v),
343 )
344 .add_rule(
345 EnvRuleBuilder::new("LOG_LEVEL")
346 .field_path("log_level")
347 .description("Log level (trace, debug, info, warn, error)")
348 .as_string(|c: &mut Self, v| c.log_level = v),
349 )
350 }
351}
352
353impl EnvConfigurable for EngineConfig {
354 fn env_overrides() -> EnvOverrides<Self> {
355 Self::env_overrides()
356 }
357}
358
359impl Validatable for EngineConfig {
360 fn validate(&self) -> crate::Result<()> {
361 let mut errors = crate::error::ValidationErrors::new();
362 self.validate_collect(&mut errors);
363 errors.into_result(())
364 }
365
366 fn validate_collect(&self, errors: &mut crate::error::ValidationErrors) {
367 if self.name.trim().is_empty() {
369 errors.add("name", "Engine name cannot be empty");
370 }
371
372 if self.max_devices == 0 {
374 errors.add("max_devices", "Max devices must be greater than 0");
375 }
376 if self.max_devices > 1_000_000 {
377 errors.add("max_devices", "Max devices cannot exceed 1,000,000");
378 }
379
380 if self.max_points == 0 {
382 errors.add("max_points", "Max points must be greater than 0");
383 }
384 if self.max_points > 100_000_000 {
385 errors.add("max_points", "Max points cannot exceed 100,000,000");
386 }
387
388 if self.tick_interval_ms < 1 {
390 errors.add("tick_interval_ms", "Tick interval must be at least 1ms");
391 }
392 if self.tick_interval_ms > 60_000 {
393 errors.add("tick_interval_ms", "Tick interval cannot exceed 60 seconds");
394 }
395
396 if self.workers == 0 {
398 errors.add("workers", "Workers must be greater than 0");
399 }
400 if self.workers > 1024 {
401 errors.add("workers", "Workers cannot exceed 1024");
402 }
403
404 if self.enable_metrics && self.metrics_interval_secs == 0 {
406 errors.add(
407 "metrics_interval_secs",
408 "Metrics interval must be greater than 0 when metrics are enabled",
409 );
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
895 .iter()
896 .any(|d| d.var_name == "TRAP_SIM_ENGINE_MAX_DEVICES"));
897 assert!(docs.iter().any(|d| d.var_name == "TRAP_SIM_LOG_LEVEL"));
898 }
899
900 #[test]
901 fn test_protocol_config_modbus_tcp() {
902 let config = ProtocolConfig::ModbusTcp(ModbusTcpConfig::default());
903 let yaml = serde_yaml::to_string(&config).unwrap();
904 assert!(yaml.contains("type: modbustcp"));
905 }
906
907 #[test]
908 fn test_protocol_config_opcua() {
909 let config = ProtocolConfig::OpcUa(OpcUaConfig::default());
910 let yaml = serde_yaml::to_string(&config).unwrap();
911 assert!(yaml.contains("type: opcua"));
912 }
913
914 #[test]
915 fn test_protocol_config_bacnet() {
916 let _config = ProtocolConfig::BacnetIp(BacnetIpConfig::default());
917 assert_eq!(BacnetIpConfig::default().device_instance, 1234);
918 }
919
920 #[test]
921 fn test_protocol_config_knx() {
922 let _config = ProtocolConfig::KnxIp(KnxIpConfig::default());
923 assert_eq!(KnxIpConfig::default().individual_address, "1.1.1");
924 }
925
926 #[test]
927 fn test_engine_config_with_protocol() {
928 let config = EngineConfig::default().with_protocol(
929 "modbus",
930 ProtocolConfig::ModbusTcp(ModbusTcpConfig::default()),
931 );
932
933 assert!(config.protocols.contains_key("modbus"));
934 }
935
936 #[test]
937 fn test_config_format_detection() {
938 assert_eq!(
939 ConfigFormat::from_path("config.yaml"),
940 Some(ConfigFormat::Yaml)
941 );
942 assert_eq!(
943 ConfigFormat::from_path("config.yml"),
944 Some(ConfigFormat::Yaml)
945 );
946 assert_eq!(
947 ConfigFormat::from_path("config.json"),
948 Some(ConfigFormat::Json)
949 );
950 assert_eq!(
951 ConfigFormat::from_path("config.toml"),
952 Some(ConfigFormat::Toml)
953 );
954 assert_eq!(ConfigFormat::from_path("config.txt"), None);
955 }
956}