Skip to main content

camel_config/
config.rs

1use camel_core::TracerConfig;
2use config::{Config, ConfigError};
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::env;
6use std::time::Duration;
7
8#[derive(Debug, Clone, Deserialize)]
9pub struct CamelConfig {
10    #[serde(default)]
11    pub routes: Vec<String>,
12
13    /// Enable file-watcher hot-reload. Defaults to false.
14    /// Can be overridden per profile in Camel.toml or via `--watch` / `--no-watch` CLI flags.
15    #[serde(default)]
16    pub watch: bool,
17
18    /// Optional stable runtime journal path for CQRS event durability/replay.
19    ///
20    /// When unset, runtime durability/replay is disabled and runtime state is ephemeral.
21    #[serde(default)]
22    pub runtime_journal_path: Option<String>,
23
24    #[serde(default = "default_log_level")]
25    pub log_level: String,
26
27    #[serde(default = "default_timeout_ms")]
28    pub timeout_ms: u64,
29
30    #[serde(default)]
31    pub components: ComponentsConfig,
32
33    #[serde(default)]
34    pub observability: ObservabilityConfig,
35
36    #[serde(default)]
37    pub supervision: Option<SupervisionCamelConfig>,
38}
39
40#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
41pub struct ComponentsConfig {
42    #[serde(default)]
43    pub timer: Option<TimerConfig>,
44
45    #[serde(default)]
46    pub http: Option<HttpCamelConfig>,
47
48    #[serde(default)]
49    pub kafka: Option<KafkaCamelConfig>,
50
51    #[serde(default)]
52    pub redis: Option<RedisCamelConfig>,
53
54    #[serde(default)]
55    pub sql: Option<SqlCamelConfig>,
56
57    #[serde(default)]
58    pub file: Option<FileCamelConfig>,
59
60    #[serde(default)]
61    pub container: Option<ContainerCamelConfig>,
62}
63
64#[derive(Debug, Clone, Deserialize, PartialEq)]
65pub struct TimerConfig {
66    #[serde(default = "default_timer_period")]
67    pub period: u64,
68}
69
70#[derive(Debug, Clone, Deserialize, PartialEq)]
71pub struct HttpCamelConfig {
72    #[serde(default = "default_http_connect_timeout_ms")]
73    pub connect_timeout_ms: u64,
74
75    #[serde(default = "default_http_response_timeout_ms")]
76    pub response_timeout_ms: u64,
77
78    #[serde(default = "default_http_max_connections")]
79    pub max_connections: usize,
80
81    #[serde(default = "default_http_max_body_size")]
82    pub max_body_size: usize,
83
84    #[serde(default = "default_http_max_request_body")]
85    pub max_request_body: usize,
86
87    #[serde(default)]
88    pub allow_private_ips: bool,
89}
90
91impl Default for HttpCamelConfig {
92    fn default() -> Self {
93        Self {
94            connect_timeout_ms: default_http_connect_timeout_ms(),
95            response_timeout_ms: default_http_response_timeout_ms(),
96            max_connections: default_http_max_connections(),
97            max_body_size: default_http_max_body_size(),
98            max_request_body: default_http_max_request_body(),
99            allow_private_ips: false,
100        }
101    }
102}
103
104#[derive(Debug, Clone, Deserialize, PartialEq)]
105pub struct KafkaCamelConfig {
106    #[serde(default = "default_kafka_brokers")]
107    pub brokers: String,
108    #[serde(default = "default_kafka_group_id")]
109    pub group_id: String,
110    #[serde(default = "default_kafka_session_timeout_ms")]
111    pub session_timeout_ms: u32,
112    #[serde(default = "default_kafka_request_timeout_ms")]
113    pub request_timeout_ms: u32,
114    #[serde(default = "default_kafka_auto_offset_reset")]
115    pub auto_offset_reset: String,
116    #[serde(default = "default_kafka_security_protocol")]
117    pub security_protocol: String,
118}
119
120impl Default for KafkaCamelConfig {
121    fn default() -> Self {
122        Self {
123            brokers: default_kafka_brokers(),
124            group_id: default_kafka_group_id(),
125            session_timeout_ms: default_kafka_session_timeout_ms(),
126            request_timeout_ms: default_kafka_request_timeout_ms(),
127            auto_offset_reset: default_kafka_auto_offset_reset(),
128            security_protocol: default_kafka_security_protocol(),
129        }
130    }
131}
132
133#[derive(Debug, Clone, Deserialize, PartialEq)]
134pub struct RedisCamelConfig {
135    #[serde(default = "default_redis_host")]
136    pub host: String,
137    #[serde(default = "default_redis_port")]
138    pub port: u16,
139}
140
141impl Default for RedisCamelConfig {
142    fn default() -> Self {
143        Self {
144            host: default_redis_host(),
145            port: default_redis_port(),
146        }
147    }
148}
149
150#[derive(Debug, Clone, Deserialize, PartialEq)]
151pub struct SqlCamelConfig {
152    #[serde(default = "default_sql_max_connections")]
153    pub max_connections: u32,
154    #[serde(default = "default_sql_min_connections")]
155    pub min_connections: u32,
156    #[serde(default = "default_sql_idle_timeout_secs")]
157    pub idle_timeout_secs: u64,
158    #[serde(default = "default_sql_max_lifetime_secs")]
159    pub max_lifetime_secs: u64,
160}
161
162impl Default for SqlCamelConfig {
163    fn default() -> Self {
164        Self {
165            max_connections: default_sql_max_connections(),
166            min_connections: default_sql_min_connections(),
167            idle_timeout_secs: default_sql_idle_timeout_secs(),
168            max_lifetime_secs: default_sql_max_lifetime_secs(),
169        }
170    }
171}
172
173#[derive(Debug, Clone, Deserialize, PartialEq)]
174pub struct FileCamelConfig {
175    #[serde(default = "default_file_delay_ms")]
176    pub delay_ms: u64,
177    #[serde(default = "default_file_initial_delay_ms")]
178    pub initial_delay_ms: u64,
179    #[serde(default = "default_file_read_timeout_ms")]
180    pub read_timeout_ms: u64,
181    #[serde(default = "default_file_write_timeout_ms")]
182    pub write_timeout_ms: u64,
183}
184
185impl Default for FileCamelConfig {
186    fn default() -> Self {
187        Self {
188            delay_ms: default_file_delay_ms(),
189            initial_delay_ms: default_file_initial_delay_ms(),
190            read_timeout_ms: default_file_read_timeout_ms(),
191            write_timeout_ms: default_file_write_timeout_ms(),
192        }
193    }
194}
195
196#[derive(Debug, Clone, Deserialize, PartialEq)]
197pub struct ContainerCamelConfig {
198    #[serde(default = "default_container_docker_host")]
199    pub docker_host: String,
200}
201
202impl Default for ContainerCamelConfig {
203    fn default() -> Self {
204        Self {
205            docker_host: default_container_docker_host(),
206        }
207    }
208}
209
210#[derive(Debug, Clone, Deserialize, PartialEq)]
211pub struct PrometheusCamelConfig {
212    #[serde(default)]
213    pub enabled: bool,
214    #[serde(default = "default_prometheus_host")]
215    pub host: String,
216    #[serde(default = "default_prometheus_port")]
217    pub port: u16,
218}
219
220impl Default for PrometheusCamelConfig {
221    fn default() -> Self {
222        Self {
223            enabled: false,
224            host: default_prometheus_host(),
225            port: default_prometheus_port(),
226        }
227    }
228}
229
230fn default_prometheus_host() -> String {
231    "0.0.0.0".to_string()
232}
233fn default_prometheus_port() -> u16 {
234    9090
235}
236
237#[derive(Debug, Clone, Deserialize, Default)]
238pub struct ObservabilityConfig {
239    #[serde(default)]
240    pub tracer: TracerConfig,
241
242    #[serde(default)]
243    pub otel: Option<OtelCamelConfig>,
244
245    #[serde(default)]
246    pub prometheus: Option<PrometheusCamelConfig>,
247}
248
249/// Protocol for OTLP export.
250#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
251#[serde(rename_all = "snake_case")]
252pub enum OtelProtocol {
253    #[default]
254    Grpc,
255    Http,
256}
257
258/// Sampling strategy.
259#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
260#[serde(rename_all = "snake_case")]
261pub enum OtelSampler {
262    #[default]
263    AlwaysOn,
264    AlwaysOff,
265    Ratio,
266}
267
268/// OpenTelemetry configuration for `[observability.otel]` in Camel.toml.
269#[derive(Debug, Clone, Deserialize, Default)]
270pub struct OtelCamelConfig {
271    #[serde(default)]
272    pub enabled: bool,
273
274    #[serde(default = "default_otel_endpoint")]
275    pub endpoint: String,
276
277    #[serde(default = "default_otel_service_name")]
278    pub service_name: String,
279
280    #[serde(default = "default_otel_log_level")]
281    pub log_level: String,
282
283    #[serde(default)]
284    pub protocol: OtelProtocol,
285
286    #[serde(default)]
287    pub sampler: OtelSampler,
288
289    #[serde(default)]
290    pub sampler_ratio: Option<f64>,
291
292    #[serde(default = "default_otel_metrics_interval_ms")]
293    pub metrics_interval_ms: u64,
294
295    #[serde(default = "default_true")]
296    pub logs_enabled: bool,
297
298    #[serde(default)]
299    pub resource_attrs: HashMap<String, String>,
300}
301
302#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
303pub struct SupervisionCamelConfig {
304    /// Maximum number of restart attempts. `None` means retry forever.
305    pub max_attempts: Option<u32>,
306
307    /// Delay before the first restart attempt in milliseconds.
308    #[serde(default = "default_initial_delay_ms")]
309    pub initial_delay_ms: u64,
310
311    /// Multiplier applied to the delay after each failed attempt.
312    #[serde(default = "default_backoff_multiplier")]
313    pub backoff_multiplier: f64,
314
315    /// Maximum delay cap between restart attempts in milliseconds.
316    #[serde(default = "default_max_delay_ms")]
317    pub max_delay_ms: u64,
318}
319
320impl Default for SupervisionCamelConfig {
321    fn default() -> Self {
322        Self {
323            max_attempts: Some(5),
324            initial_delay_ms: 1000,
325            backoff_multiplier: 2.0,
326            max_delay_ms: 60000,
327        }
328    }
329}
330
331impl SupervisionCamelConfig {
332    /// Convert to camel_api::SupervisionConfig
333    pub fn into_supervision_config(self) -> camel_api::SupervisionConfig {
334        camel_api::SupervisionConfig {
335            max_attempts: self.max_attempts,
336            initial_delay: Duration::from_millis(self.initial_delay_ms),
337            backoff_multiplier: self.backoff_multiplier,
338            max_delay: Duration::from_millis(self.max_delay_ms),
339        }
340    }
341}
342
343fn default_log_level() -> String {
344    "INFO".to_string()
345}
346fn default_timeout_ms() -> u64 {
347    5000
348}
349fn default_timer_period() -> u64 {
350    1000
351}
352fn default_http_connect_timeout_ms() -> u64 {
353    5_000
354}
355fn default_http_response_timeout_ms() -> u64 {
356    30_000
357}
358fn default_http_max_connections() -> usize {
359    100
360}
361fn default_http_max_body_size() -> usize {
362    10_485_760
363}
364fn default_http_max_request_body() -> usize {
365    2_097_152
366}
367
368fn default_kafka_brokers() -> String {
369    "localhost:9092".to_string()
370}
371fn default_kafka_group_id() -> String {
372    "camel".to_string()
373}
374fn default_kafka_session_timeout_ms() -> u32 {
375    45_000
376}
377fn default_kafka_request_timeout_ms() -> u32 {
378    30_000
379}
380fn default_kafka_auto_offset_reset() -> String {
381    "latest".to_string()
382}
383fn default_kafka_security_protocol() -> String {
384    "plaintext".to_string()
385}
386
387fn default_redis_host() -> String {
388    "localhost".to_string()
389}
390fn default_redis_port() -> u16 {
391    6379
392}
393
394fn default_sql_max_connections() -> u32 {
395    5
396}
397fn default_sql_min_connections() -> u32 {
398    1
399}
400fn default_sql_idle_timeout_secs() -> u64 {
401    300
402}
403fn default_sql_max_lifetime_secs() -> u64 {
404    1_800
405}
406
407fn default_file_delay_ms() -> u64 {
408    500
409}
410fn default_file_initial_delay_ms() -> u64 {
411    1_000
412}
413fn default_file_read_timeout_ms() -> u64 {
414    30_000
415}
416fn default_file_write_timeout_ms() -> u64 {
417    30_000
418}
419
420fn default_container_docker_host() -> String {
421    "unix:///var/run/docker.sock".to_string()
422}
423
424fn default_otel_endpoint() -> String {
425    "http://localhost:4317".to_string()
426}
427fn default_otel_service_name() -> String {
428    "rust-camel".to_string()
429}
430fn default_otel_log_level() -> String {
431    "info".to_string()
432}
433fn default_otel_metrics_interval_ms() -> u64 {
434    60000
435}
436fn default_true() -> bool {
437    true
438}
439
440fn default_initial_delay_ms() -> u64 {
441    1000
442}
443
444fn default_backoff_multiplier() -> f64 {
445    2.0
446}
447
448fn default_max_delay_ms() -> u64 {
449    60000
450}
451
452/// Deep merge two TOML values
453/// Tables are merged recursively, with overlay values taking precedence
454fn merge_toml_values(base: &mut toml::Value, overlay: &toml::Value) {
455    match (base, overlay) {
456        (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
457            for (key, value) in overlay_table {
458                if let Some(base_value) = base_table.get_mut(key) {
459                    // Both have this key - merge recursively
460                    merge_toml_values(base_value, value);
461                } else {
462                    // Only overlay has this key - insert it
463                    base_table.insert(key.clone(), value.clone());
464                }
465            }
466        }
467        // For non-table values, overlay replaces base entirely
468        (base, overlay) => {
469            *base = overlay.clone();
470        }
471    }
472}
473
474impl CamelConfig {
475    pub fn from_file(path: &str) -> Result<Self, ConfigError> {
476        Self::from_file_with_profile(path, None)
477    }
478
479    pub fn from_file_with_env(path: &str) -> Result<Self, ConfigError> {
480        Self::from_file_with_profile_and_env(path, None)
481    }
482
483    pub fn from_file_with_profile(path: &str, profile: Option<&str>) -> Result<Self, ConfigError> {
484        // Get profile from parameter or environment variable
485        let env_profile = env::var("CAMEL_PROFILE").ok();
486        let profile = profile.or(env_profile.as_deref());
487
488        // Read the TOML file as a generic value for deep merging
489        let content = std::fs::read_to_string(path)
490            .map_err(|e| ConfigError::Message(format!("Failed to read config file: {}", e)))?;
491        let mut config_value: toml::Value = toml::from_str(&content)
492            .map_err(|e| ConfigError::Message(format!("Failed to parse TOML: {}", e)))?;
493
494        // If a profile is specified, merge it with default
495        if let Some(p) = profile {
496            // Extract default config as base
497            let default_value = config_value.get("default").cloned();
498
499            // Extract profile config
500            let profile_value = config_value.get(p).cloned();
501
502            if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
503                // Deep merge profile onto default
504                merge_toml_values(&mut base, &overlay);
505
506                // Replace the entire config with the merged result
507                config_value = base;
508            } else if let Some(profile_val) = config_value.get(p).cloned() {
509                // No default, just use profile
510                config_value = profile_val;
511            } else {
512                return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
513            }
514        } else {
515            // No profile specified, use default section if it exists
516            if let Some(default_val) = config_value.get("default").cloned() {
517                config_value = default_val;
518            }
519        }
520
521        // Deserialize the merged config
522        let merged_toml = toml::to_string(&config_value).map_err(|e| {
523            ConfigError::Message(format!("Failed to serialize merged config: {}", e))
524        })?;
525
526        let config = Config::builder()
527            .add_source(config::File::from_str(
528                &merged_toml,
529                config::FileFormat::Toml,
530            ))
531            .build()?;
532
533        config.try_deserialize()
534    }
535
536    pub fn from_file_with_profile_and_env(
537        path: &str,
538        profile: Option<&str>,
539    ) -> Result<Self, ConfigError> {
540        // Get profile from parameter or environment variable
541        let env_profile = env::var("CAMEL_PROFILE").ok();
542        let profile = profile.or(env_profile.as_deref());
543
544        // Read the TOML file as a generic value for deep merging
545        let content = std::fs::read_to_string(path)
546            .map_err(|e| ConfigError::Message(format!("Failed to read config file: {}", e)))?;
547        let mut config_value: toml::Value = toml::from_str(&content)
548            .map_err(|e| ConfigError::Message(format!("Failed to parse TOML: {}", e)))?;
549
550        // If a profile is specified, merge it with default
551        if let Some(p) = profile {
552            // Extract default config as base
553            let default_value = config_value.get("default").cloned();
554
555            // Extract profile config
556            let profile_value = config_value.get(p).cloned();
557
558            if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
559                // Deep merge profile onto default
560                merge_toml_values(&mut base, &overlay);
561
562                // Replace the entire config with the merged result
563                config_value = base;
564            } else if let Some(profile_val) = config_value.get(p).cloned() {
565                // No default, just use profile
566                config_value = profile_val;
567            } else {
568                return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
569            }
570        } else {
571            // No profile specified, use default section if it exists
572            if let Some(default_val) = config_value.get("default").cloned() {
573                config_value = default_val;
574            }
575        }
576
577        // Deserialize the merged config and apply environment variables
578        let merged_toml = toml::to_string(&config_value).map_err(|e| {
579            ConfigError::Message(format!("Failed to serialize merged config: {}", e))
580        })?;
581
582        let config = Config::builder()
583            .add_source(config::File::from_str(
584                &merged_toml,
585                config::FileFormat::Toml,
586            ))
587            .add_source(config::Environment::with_prefix("CAMEL").try_parsing(true))
588            .build()?;
589
590        config.try_deserialize()
591    }
592
593    pub fn from_env_or_default() -> Result<Self, ConfigError> {
594        let path = env::var("CAMEL_CONFIG_FILE").unwrap_or_else(|_| "Camel.toml".to_string());
595
596        Self::from_file(&path)
597    }
598}
599
600#[cfg(test)]
601mod http_camel_config_tests {
602    use super::*;
603
604    fn parse(toml: &str) -> CamelConfig {
605        let cfg = config::Config::builder()
606            .add_source(config::File::from_str(toml, config::FileFormat::Toml))
607            .build()
608            .unwrap();
609        cfg.try_deserialize().unwrap()
610    }
611
612    #[test]
613    fn test_http_camel_config_defaults() {
614        let cfg = parse("");
615        assert!(cfg.components.http.is_none());
616    }
617
618    #[test]
619    fn test_http_camel_config_default_matches_serde() {
620        let default = HttpCamelConfig::default();
621        assert_eq!(default.connect_timeout_ms, 5_000);
622        assert_eq!(default.response_timeout_ms, 30_000);
623        assert_eq!(default.max_connections, 100);
624        assert_eq!(default.max_body_size, 10_485_760);
625        assert_eq!(default.max_request_body, 2_097_152);
626        assert!(!default.allow_private_ips);
627    }
628
629    #[test]
630    fn test_http_camel_config_partial_override() {
631        let cfg = parse(
632            r#"
633[components.http]
634connect_timeout_ms = 1000
635"#,
636        );
637        let http = cfg.components.http.unwrap();
638        assert_eq!(http.connect_timeout_ms, 1000);
639        assert_eq!(http.response_timeout_ms, 30_000);
640        assert_eq!(http.max_connections, 100);
641        assert_eq!(http.max_body_size, 10_485_760);
642        assert_eq!(http.max_request_body, 2_097_152);
643        assert!(!http.allow_private_ips);
644    }
645
646    #[test]
647    fn test_http_camel_config_all_fields() {
648        let cfg = parse(
649            r#"
650[components.http]
651connect_timeout_ms = 2000
652response_timeout_ms = 60000
653max_connections = 50
654max_body_size = 5242880
655max_request_body = 1048576
656allow_private_ips = true
657"#,
658        );
659        let http = cfg.components.http.unwrap();
660        assert_eq!(http.connect_timeout_ms, 2000);
661        assert_eq!(http.response_timeout_ms, 60000);
662        assert_eq!(http.max_connections, 50);
663        assert_eq!(http.max_body_size, 5_242_880);
664        assert_eq!(http.max_request_body, 1_048_576);
665        assert!(http.allow_private_ips);
666    }
667}
668
669#[cfg(test)]
670mod component_camel_config_tests {
671    use super::*;
672
673    fn parse(toml: &str) -> CamelConfig {
674        let cfg = config::Config::builder()
675            .add_source(config::File::from_str(toml, config::FileFormat::Toml))
676            .build()
677            .unwrap();
678        cfg.try_deserialize().unwrap()
679    }
680
681    #[test]
682    fn test_kafka_defaults() {
683        let cfg = parse("");
684        assert!(cfg.components.kafka.is_none());
685    }
686
687    #[test]
688    fn test_kafka_partial_override() {
689        let cfg = parse(
690            r#"
691[components.kafka]
692brokers = "prod:9092"
693"#,
694        );
695        let k = cfg.components.kafka.unwrap();
696        assert_eq!(k.brokers, "prod:9092");
697        assert_eq!(k.group_id, "camel");
698        assert_eq!(k.session_timeout_ms, 45_000);
699        assert_eq!(k.request_timeout_ms, 30_000);
700        assert_eq!(k.auto_offset_reset, "latest");
701        assert_eq!(k.security_protocol, "plaintext");
702    }
703
704    #[test]
705    fn test_redis_defaults() {
706        let cfg = parse(
707            r#"
708[components.redis]
709port = 6380
710"#,
711        );
712        let r = cfg.components.redis.unwrap();
713        assert_eq!(r.host, "localhost");
714        assert_eq!(r.port, 6380);
715    }
716
717    #[test]
718    fn test_sql_defaults() {
719        let cfg = parse(
720            r#"
721[components.sql]
722max_connections = 10
723"#,
724        );
725        let s = cfg.components.sql.unwrap();
726        assert_eq!(s.max_connections, 10);
727        assert_eq!(s.min_connections, 1);
728        assert_eq!(s.idle_timeout_secs, 300);
729        assert_eq!(s.max_lifetime_secs, 1_800);
730    }
731
732    #[test]
733    fn test_file_defaults() {
734        let cfg = parse(
735            r#"
736[components.file]
737delay_ms = 1000
738"#,
739        );
740        let f = cfg.components.file.unwrap();
741        assert_eq!(f.delay_ms, 1000);
742        assert_eq!(f.initial_delay_ms, 1_000);
743        assert_eq!(f.read_timeout_ms, 30_000);
744        assert_eq!(f.write_timeout_ms, 30_000);
745    }
746
747    #[test]
748    fn test_container_defaults() {
749        let cfg = parse(
750            r#"
751[components.container]
752docker_host = "tcp://remote:2375"
753"#,
754        );
755        let c = cfg.components.container.unwrap();
756        assert_eq!(c.docker_host, "tcp://remote:2375");
757    }
758
759    #[test]
760    fn test_omitted_sections_are_none() {
761        let cfg = parse("");
762        assert!(cfg.components.kafka.is_none());
763        assert!(cfg.components.redis.is_none());
764        assert!(cfg.components.sql.is_none());
765        assert!(cfg.components.file.is_none());
766        assert!(cfg.components.container.is_none());
767    }
768
769    #[test]
770    fn test_kafka_camel_config_default_matches_serde() {
771        let d = KafkaCamelConfig::default();
772        assert_eq!(d.brokers, "localhost:9092");
773        assert_eq!(d.group_id, "camel");
774        assert_eq!(d.session_timeout_ms, 45_000);
775        assert_eq!(d.request_timeout_ms, 30_000);
776        assert_eq!(d.auto_offset_reset, "latest");
777        assert_eq!(d.security_protocol, "plaintext");
778    }
779
780    #[test]
781    fn test_redis_camel_config_default_matches_serde() {
782        let d = RedisCamelConfig::default();
783        assert_eq!(d.host, "localhost");
784        assert_eq!(d.port, 6379);
785    }
786
787    #[test]
788    fn test_sql_camel_config_default_matches_serde() {
789        let d = SqlCamelConfig::default();
790        assert_eq!(d.max_connections, 5);
791        assert_eq!(d.min_connections, 1);
792        assert_eq!(d.idle_timeout_secs, 300);
793        assert_eq!(d.max_lifetime_secs, 1_800);
794    }
795
796    #[test]
797    fn test_file_camel_config_default_matches_serde() {
798        let d = FileCamelConfig::default();
799        assert_eq!(d.delay_ms, 500);
800        assert_eq!(d.initial_delay_ms, 1_000);
801        assert_eq!(d.read_timeout_ms, 30_000);
802        assert_eq!(d.write_timeout_ms, 30_000);
803    }
804
805    #[test]
806    fn test_container_camel_config_default_matches_serde() {
807        let d = ContainerCamelConfig::default();
808        assert_eq!(d.docker_host, "unix:///var/run/docker.sock");
809    }
810}
811
812#[cfg(test)]
813mod prometheus_config_tests {
814    use super::*;
815
816    fn parse(toml: &str) -> CamelConfig {
817        let cfg = config::Config::builder()
818            .add_source(config::File::from_str(toml, config::FileFormat::Toml))
819            .build()
820            .unwrap();
821        cfg.try_deserialize().unwrap()
822    }
823
824    #[test]
825    fn test_prometheus_absent_is_none() {
826        let cfg = parse("");
827        assert!(cfg.observability.prometheus.is_none());
828    }
829
830    #[test]
831    fn test_prometheus_defaults() {
832        let cfg = parse(
833            r#"
834[observability.prometheus]
835enabled = true
836"#,
837        );
838        let p = cfg.observability.prometheus.unwrap();
839        assert!(p.enabled);
840        assert_eq!(p.host, "0.0.0.0");
841        assert_eq!(p.port, 9090);
842    }
843
844    #[test]
845    fn test_prometheus_full() {
846        let cfg = parse(
847            r#"
848[observability.prometheus]
849enabled = true
850host = "127.0.0.1"
851port = 9091
852"#,
853        );
854        let p = cfg.observability.prometheus.unwrap();
855        assert_eq!(p.host, "127.0.0.1");
856        assert_eq!(p.port, 9091);
857    }
858}
859
860#[cfg(all(test, feature = "http"))]
861mod http_from_tests {
862    use crate::config::HttpCamelConfig;
863    use camel_component_http;
864
865    #[test]
866    fn test_http_camel_config_to_http_config() {
867        let camel_cfg = HttpCamelConfig {
868            connect_timeout_ms: 1_000,
869            response_timeout_ms: 5_000,
870            max_connections: 20,
871            max_body_size: 1_000,
872            max_request_body: 500,
873            allow_private_ips: true,
874        };
875        let cfg = camel_component_http::HttpConfig::from(&camel_cfg);
876        assert_eq!(cfg.connect_timeout_ms, 1_000);
877        assert_eq!(cfg.max_connections, 20);
878        assert!(cfg.allow_private_ips);
879    }
880}