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 #[serde(default)]
16 pub watch: bool,
17
18 #[serde(default)]
22 pub runtime_journal: Option<JournalConfig>,
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 = "default_drain_timeout_ms")]
31 pub drain_timeout_ms: u64,
32
33 #[serde(default = "default_watch_debounce_ms")]
34 pub watch_debounce_ms: u64,
35
36 #[serde(default)]
37 pub components: ComponentsConfig,
38
39 #[serde(default)]
40 pub observability: ObservabilityConfig,
41
42 #[serde(default)]
43 pub supervision: Option<SupervisionCamelConfig>,
44
45 #[serde(default)]
46 pub platform: PlatformCamelConfig,
47
48 #[serde(default)]
49 pub stream_caching: StreamCachingConfig,
50
51 #[serde(default)]
52 pub beans: HashMap<String, BeanConfig>,
53}
54
55#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
59#[serde(tag = "type", rename_all = "snake_case")]
60pub enum PlatformCamelConfig {
61 #[default]
62 Noop,
63 Kubernetes(KubernetesPlatformCamelConfig),
64}
65
66#[derive(Debug, Clone, Deserialize, PartialEq)]
68pub struct KubernetesPlatformCamelConfig {
69 #[serde(default)]
70 pub namespace: Option<String>,
71 #[serde(default = "default_lease_name_prefix")]
72 pub lease_name_prefix: String,
73 #[serde(default = "default_lease_duration_secs")]
74 pub lease_duration_secs: u64,
75 #[serde(default = "default_renew_deadline_secs")]
76 pub renew_deadline_secs: u64,
77 #[serde(default = "default_retry_period_secs")]
78 pub retry_period_secs: u64,
79 #[serde(default = "default_kubernetes_jitter_factor")]
80 pub jitter_factor: f64,
81}
82
83impl Default for KubernetesPlatformCamelConfig {
84 fn default() -> Self {
85 Self {
86 namespace: None,
87 lease_name_prefix: default_lease_name_prefix(),
88 lease_duration_secs: default_lease_duration_secs(),
89 renew_deadline_secs: default_renew_deadline_secs(),
90 retry_period_secs: default_retry_period_secs(),
91 jitter_factor: default_kubernetes_jitter_factor(),
92 }
93 }
94}
95
96fn default_lease_name_prefix() -> String {
97 "camel-".to_string()
98}
99fn default_lease_duration_secs() -> u64 {
100 15
101}
102fn default_renew_deadline_secs() -> u64 {
103 10
104}
105fn default_retry_period_secs() -> u64 {
106 2
107}
108fn default_kubernetes_jitter_factor() -> f64 {
109 0.2
110}
111
112#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
113pub struct ComponentsConfig {
114 #[serde(flatten)]
117 pub raw: HashMap<String, toml::Value>,
118}
119
120#[derive(Debug, Clone, Deserialize, PartialEq)]
121pub struct PrometheusCamelConfig {
122 #[serde(default)]
123 pub enabled: bool,
124 #[serde(default = "default_prometheus_host")]
125 pub host: String,
126 #[serde(default = "default_prometheus_port")]
127 pub port: u16,
128}
129
130impl Default for PrometheusCamelConfig {
131 fn default() -> Self {
132 Self {
133 enabled: false,
134 host: default_prometheus_host(),
135 port: default_prometheus_port(),
136 }
137 }
138}
139
140fn default_prometheus_host() -> String {
141 "0.0.0.0".to_string()
142}
143fn default_prometheus_port() -> u16 {
144 9090
145}
146
147#[derive(Debug, Clone, Deserialize, PartialEq)]
148pub struct HealthCamelConfig {
149 #[serde(default)]
150 pub enabled: bool,
151 #[serde(default = "default_health_host")]
152 pub host: String,
153 #[serde(default = "default_health_port")]
154 pub port: u16,
155}
156
157impl Default for HealthCamelConfig {
158 fn default() -> Self {
159 Self {
160 enabled: false,
161 host: default_health_host(),
162 port: default_health_port(),
163 }
164 }
165}
166
167fn default_health_host() -> String {
168 "0.0.0.0".to_string()
169}
170
171fn default_health_port() -> u16 {
172 8081
173}
174
175#[derive(Debug, Clone, Deserialize, Default)]
176pub struct ObservabilityConfig {
177 #[serde(default)]
178 pub tracer: TracerConfig,
179
180 #[serde(default)]
181 pub otel: Option<OtelCamelConfig>,
182
183 #[serde(default)]
184 pub prometheus: Option<PrometheusCamelConfig>,
185
186 #[serde(default)]
187 pub health: Option<HealthCamelConfig>,
188}
189
190#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
192#[serde(rename_all = "snake_case")]
193pub enum OtelProtocol {
194 #[default]
195 Grpc,
196 Http,
197}
198
199#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
201#[serde(rename_all = "snake_case")]
202pub enum OtelSampler {
203 #[default]
204 AlwaysOn,
205 AlwaysOff,
206 Ratio,
207}
208
209#[derive(Debug, Clone, Deserialize, Default)]
211pub struct OtelCamelConfig {
212 #[serde(default)]
213 pub enabled: bool,
214
215 #[serde(default = "default_otel_endpoint")]
216 pub endpoint: String,
217
218 #[serde(default = "default_otel_service_name")]
219 pub service_name: String,
220
221 #[serde(default = "default_otel_log_level")]
222 pub log_level: String,
223
224 #[serde(default)]
225 pub protocol: OtelProtocol,
226
227 #[serde(default)]
228 pub sampler: OtelSampler,
229
230 #[serde(default)]
231 pub sampler_ratio: Option<f64>,
232
233 #[serde(default = "default_otel_metrics_interval_ms")]
234 pub metrics_interval_ms: u64,
235
236 #[serde(default = "default_true")]
237 pub logs_enabled: bool,
238
239 #[serde(default)]
240 pub resource_attrs: HashMap<String, String>,
241}
242
243#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
244pub struct SupervisionCamelConfig {
245 pub max_attempts: Option<u32>,
247
248 #[serde(default = "default_initial_delay_ms")]
250 pub initial_delay_ms: u64,
251
252 #[serde(default = "default_backoff_multiplier")]
254 pub backoff_multiplier: f64,
255
256 #[serde(default = "default_max_delay_ms")]
258 pub max_delay_ms: u64,
259}
260
261impl Default for SupervisionCamelConfig {
262 fn default() -> Self {
263 Self {
264 max_attempts: Some(5),
265 initial_delay_ms: 1000,
266 backoff_multiplier: 2.0,
267 max_delay_ms: 60000,
268 }
269 }
270}
271
272impl SupervisionCamelConfig {
273 pub fn into_supervision_config(self) -> camel_api::SupervisionConfig {
275 camel_api::SupervisionConfig {
276 max_attempts: self.max_attempts,
277 initial_delay: Duration::from_millis(self.initial_delay_ms),
278 backoff_multiplier: self.backoff_multiplier,
279 max_delay: Duration::from_millis(self.max_delay_ms),
280 }
281 }
282}
283
284#[derive(Debug, Clone, Deserialize, PartialEq, Default)]
290#[serde(rename_all = "snake_case")]
291pub enum JournalDurability {
292 #[default]
294 Immediate,
295 Eventual,
297}
298
299impl From<JournalDurability> for camel_core::JournalDurability {
300 fn from(d: JournalDurability) -> Self {
301 match d {
302 JournalDurability::Immediate => camel_core::JournalDurability::Immediate,
303 JournalDurability::Eventual => camel_core::JournalDurability::Eventual,
304 }
305 }
306}
307
308fn default_compaction_threshold_events() -> u64 {
309 10_000
310}
311
312#[derive(Debug, Clone, Deserialize, PartialEq)]
314pub struct JournalConfig {
315 pub path: std::path::PathBuf,
317
318 #[serde(default)]
320 pub durability: JournalDurability,
321
322 #[serde(default = "default_compaction_threshold_events")]
324 pub compaction_threshold_events: u64,
325}
326
327#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
328pub struct StreamCachingConfig {
329 #[serde(default = "default_stream_cache_threshold")]
330 pub threshold: usize,
331}
332
333fn default_stream_cache_threshold() -> usize {
334 camel_api::stream_cache::DEFAULT_STREAM_CACHE_THRESHOLD
335}
336
337impl Default for StreamCachingConfig {
338 fn default() -> Self {
339 Self {
340 threshold: default_stream_cache_threshold(),
341 }
342 }
343}
344
345#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
346pub struct BeanConfig {
347 pub plugin: String,
348}
349
350impl From<&JournalConfig> for camel_core::RedbJournalOptions {
351 fn from(cfg: &JournalConfig) -> Self {
352 camel_core::RedbJournalOptions {
353 durability: cfg.durability.clone().into(),
354 compaction_threshold_events: cfg.compaction_threshold_events,
355 }
356 }
357}
358
359fn default_log_level() -> String {
360 "INFO".to_string()
361}
362fn default_timeout_ms() -> u64 {
363 5000
364}
365fn default_drain_timeout_ms() -> u64 {
366 10_000
367}
368fn default_watch_debounce_ms() -> u64 {
369 300
370}
371
372fn default_otel_endpoint() -> String {
373 "http://localhost:4317".to_string()
374}
375fn default_otel_service_name() -> String {
376 "rust-camel".to_string()
377}
378fn default_otel_log_level() -> String {
379 "info".to_string()
380}
381fn default_otel_metrics_interval_ms() -> u64 {
382 60000
383}
384fn default_true() -> bool {
385 true
386}
387
388fn default_initial_delay_ms() -> u64 {
389 1000
390}
391
392fn default_backoff_multiplier() -> f64 {
393 2.0
394}
395
396fn default_max_delay_ms() -> u64 {
397 60000
398}
399
400fn merge_toml_values(base: &mut toml::Value, overlay: &toml::Value) {
403 match (base, overlay) {
404 (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
405 for (key, value) in overlay_table {
406 if let Some(base_value) = base_table.get_mut(key) {
407 merge_toml_values(base_value, value);
409 } else {
410 base_table.insert(key.clone(), value.clone());
412 }
413 }
414 }
415 (base, overlay) => {
417 *base = overlay.clone();
418 }
419 }
420}
421
422impl CamelConfig {
423 pub fn from_file(path: &str) -> Result<Self, ConfigError> {
424 Self::from_file_with_profile(path, None)
425 }
426
427 pub fn from_file_with_env(path: &str) -> Result<Self, ConfigError> {
428 Self::from_file_with_profile_and_env(path, None)
429 }
430
431 pub fn from_file_with_profile(path: &str, profile: Option<&str>) -> Result<Self, ConfigError> {
432 let env_profile = env::var("CAMEL_PROFILE").ok();
434 let profile = profile.or(env_profile.as_deref());
435
436 let content = std::fs::read_to_string(path)
438 .map_err(|e| ConfigError::Message(format!("Failed to read config file: {}", e)))?;
439 let mut config_value: toml::Value = toml::from_str(&content)
440 .map_err(|e| ConfigError::Message(format!("Failed to parse TOML: {}", e)))?;
441
442 if let Some(p) = profile {
444 let default_value = config_value.get("default").cloned();
446
447 let profile_value = config_value.get(p).cloned();
449
450 if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
451 merge_toml_values(&mut base, &overlay);
453
454 config_value = base;
456 } else if let Some(profile_val) = config_value.get(p).cloned() {
457 config_value = profile_val;
459 } else {
460 return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
461 }
462 } else {
463 if let Some(default_val) = config_value.get("default").cloned() {
465 config_value = default_val;
466 }
467 }
468
469 let merged_toml = toml::to_string(&config_value).map_err(|e| {
471 ConfigError::Message(format!("Failed to serialize merged config: {}", e))
472 })?;
473
474 let config = Config::builder()
475 .add_source(config::File::from_str(
476 &merged_toml,
477 config::FileFormat::Toml,
478 ))
479 .build()?;
480
481 config.try_deserialize()
482 }
483
484 pub fn from_file_with_profile_and_env(
485 path: &str,
486 profile: Option<&str>,
487 ) -> Result<Self, ConfigError> {
488 let env_profile = env::var("CAMEL_PROFILE").ok();
490 let profile = profile.or(env_profile.as_deref());
491
492 let content = std::fs::read_to_string(path)
494 .map_err(|e| ConfigError::Message(format!("Failed to read config file: {}", e)))?;
495 let mut config_value: toml::Value = toml::from_str(&content)
496 .map_err(|e| ConfigError::Message(format!("Failed to parse TOML: {}", e)))?;
497
498 if let Some(p) = profile {
500 let default_value = config_value.get("default").cloned();
502
503 let profile_value = config_value.get(p).cloned();
505
506 if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
507 merge_toml_values(&mut base, &overlay);
509
510 config_value = base;
512 } else if let Some(profile_val) = config_value.get(p).cloned() {
513 config_value = profile_val;
515 } else {
516 return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
517 }
518 } else {
519 if let Some(default_val) = config_value.get("default").cloned() {
521 config_value = default_val;
522 }
523 }
524
525 let merged_toml = toml::to_string(&config_value).map_err(|e| {
527 ConfigError::Message(format!("Failed to serialize merged config: {}", e))
528 })?;
529
530 let config = Config::builder()
531 .add_source(config::File::from_str(
532 &merged_toml,
533 config::FileFormat::Toml,
534 ))
535 .add_source(config::Environment::with_prefix("CAMEL").try_parsing(true))
536 .build()?;
537
538 config.try_deserialize()
539 }
540
541 pub fn from_env_or_default() -> Result<Self, ConfigError> {
542 let path = env::var("CAMEL_CONFIG_FILE").unwrap_or_else(|_| "Camel.toml".to_string());
543
544 Self::from_file(&path)
545 }
546}
547
548#[cfg(test)]
549mod camel_config_defaults_tests {
550 use super::*;
551
552 #[test]
553 fn watch_debounce_ms_default_is_300() {
554 let config: CamelConfig = toml::from_str("").unwrap();
555 assert_eq!(config.watch_debounce_ms, 300);
556 }
557
558 #[test]
559 fn watch_debounce_ms_custom_value() {
560 let config: CamelConfig = toml::from_str("watch_debounce_ms = 50").unwrap();
561 assert_eq!(config.watch_debounce_ms, 50);
562 }
563
564 #[test]
565 fn stream_caching_default_threshold_is_set() {
566 let config: CamelConfig = toml::from_str("").unwrap();
567 assert_eq!(
568 config.stream_caching.threshold,
569 camel_api::stream_cache::DEFAULT_STREAM_CACHE_THRESHOLD
570 );
571 }
572
573 #[test]
574 fn stream_caching_custom_threshold_value() {
575 let config: CamelConfig = toml::from_str("[stream_caching]\nthreshold = 1234").unwrap();
576 assert_eq!(config.stream_caching.threshold, 1234);
577 }
578}
579
580#[cfg(test)]
581mod components_config_tests {
582 use super::*;
583
584 #[test]
585 fn components_config_deserializes_raw_toml_block() {
586 let toml_str = r#"
587 [kafka]
588 brokers = ["localhost:9092"]
589
590 [redis]
591 host = "redis.local"
592 "#;
593 let cfg: ComponentsConfig = toml::from_str(toml_str).unwrap();
594 assert!(cfg.raw.contains_key("kafka"));
595 assert!(cfg.raw.contains_key("redis"));
596 }
597}
598
599#[cfg(test)]
600mod prometheus_config_tests {
601 use super::*;
602
603 fn parse(toml: &str) -> CamelConfig {
604 let cfg = config::Config::builder()
605 .add_source(config::File::from_str(toml, config::FileFormat::Toml))
606 .build()
607 .unwrap();
608 cfg.try_deserialize().unwrap()
609 }
610
611 #[test]
612 fn test_prometheus_absent_is_none() {
613 let cfg = parse("");
614 assert!(cfg.observability.prometheus.is_none());
615 }
616
617 #[test]
618 fn test_prometheus_defaults() {
619 let cfg = parse(
620 r#"
621[observability.prometheus]
622enabled = true
623"#,
624 );
625 let p = cfg.observability.prometheus.unwrap();
626 assert!(p.enabled);
627 assert_eq!(p.host, "0.0.0.0");
628 assert_eq!(p.port, 9090);
629 }
630
631 #[test]
632 fn test_prometheus_full() {
633 let cfg = parse(
634 r#"
635[observability.prometheus]
636enabled = true
637host = "127.0.0.1"
638port = 9091
639"#,
640 );
641 let p = cfg.observability.prometheus.unwrap();
642 assert_eq!(p.host, "127.0.0.1");
643 assert_eq!(p.port, 9091);
644 }
645
646 #[test]
647 fn test_health_config_defaults() {
648 let cfg = parse(
649 r#"
650[observability.health]
651enabled = true
652"#,
653 );
654 let h = cfg.observability.health.unwrap();
655 assert!(h.enabled);
656 assert_eq!(h.host, "0.0.0.0");
657 assert_eq!(h.port, 8081);
658 }
659
660 #[test]
661 fn test_health_config_custom_port() {
662 let cfg = parse(
663 r#"
664[observability.health]
665enabled = true
666port = 9091
667"#,
668 );
669 let h = cfg.observability.health.unwrap();
670 assert_eq!(h.port, 9091);
671 assert_eq!(h.host, "0.0.0.0");
672 }
673}
674
675#[cfg(test)]
676mod platform_config_tests {
677 use super::*;
678
679 fn parse(toml: &str) -> CamelConfig {
680 let cfg = config::Config::builder()
681 .add_source(config::File::from_str(toml, config::FileFormat::Toml))
682 .build()
683 .unwrap();
684 cfg.try_deserialize().unwrap()
685 }
686
687 #[test]
688 fn platform_default_is_noop() {
689 let cfg = parse("");
690 assert!(matches!(cfg.platform, PlatformCamelConfig::Noop));
691 }
692
693 #[test]
694 fn platform_parses_kubernetes_from_toml() {
695 let cfg = parse(
696 r#"
697[platform]
698type = "kubernetes"
699namespace = "team-a"
700lease_name_prefix = "camel-"
701lease_duration_secs = 15
702renew_deadline_secs = 10
703retry_period_secs = 2
704jitter_factor = 0.2
705"#,
706 );
707 match cfg.platform {
708 PlatformCamelConfig::Kubernetes(k8s) => {
709 assert_eq!(k8s.namespace.as_deref(), Some("team-a"));
710 assert_eq!(k8s.lease_name_prefix, "camel-");
711 assert_eq!(k8s.lease_duration_secs, 15);
712 assert_eq!(k8s.renew_deadline_secs, 10);
713 assert_eq!(k8s.retry_period_secs, 2);
714 assert!((k8s.jitter_factor - 0.2).abs() < f64::EPSILON);
715 }
716 other => panic!("expected Kubernetes, got {:?}", other),
717 }
718 }
719
720 #[test]
721 fn platform_kubernetes_defaults() {
722 let cfg = parse(
723 r#"
724[platform]
725type = "kubernetes"
726"#,
727 );
728 match cfg.platform {
729 PlatformCamelConfig::Kubernetes(k8s) => {
730 assert!(k8s.namespace.is_none());
731 assert_eq!(k8s.lease_name_prefix, "camel-");
732 assert_eq!(k8s.lease_duration_secs, 15);
733 assert_eq!(k8s.renew_deadline_secs, 10);
734 assert_eq!(k8s.retry_period_secs, 2);
735 assert!((k8s.jitter_factor - 0.2).abs() < f64::EPSILON);
736 }
737 other => panic!("expected Kubernetes, got {:?}", other),
738 }
739 }
740
741 #[test]
742 fn platform_parses_kubernetes_from_file_with_profile() {
743 use std::io::Write;
744 let mut f = tempfile::NamedTempFile::new().expect("temp file");
745 f.write_all(
746 br#"
747[default]
748[default.platform]
749type = "kubernetes"
750namespace = "production"
751
752[dev]
753[dev.platform]
754type = "noop"
755"#,
756 )
757 .expect("write config");
758
759 let cfg_prod =
760 CamelConfig::from_file_with_profile(f.path().to_str().unwrap(), Some("default"))
761 .expect("prod config");
762 assert!(matches!(
763 cfg_prod.platform,
764 PlatformCamelConfig::Kubernetes(_)
765 ));
766
767 let cfg_dev = CamelConfig::from_file_with_profile(f.path().to_str().unwrap(), Some("dev"))
768 .expect("dev config");
769 assert!(matches!(cfg_dev.platform, PlatformCamelConfig::Noop));
770 }
771}
772
773#[cfg(test)]
774mod profile_loading_tests {
775 use super::*;
776
777 fn write_temp_config(contents: &str) -> tempfile::NamedTempFile {
778 use std::io::Write;
779 let mut f = tempfile::NamedTempFile::new().expect("temp file");
780 f.write_all(contents.as_bytes()).expect("write config");
781 f
782 }
783
784 #[test]
785 fn test_merge_toml_values_merges_nested_tables() {
786 let mut base: toml::Value = toml::from_str(
787 r#"
788[components.http]
789connect_timeout_ms = 1000
790pool_max_idle_per_host = 50
791"#,
792 )
793 .unwrap();
794
795 let overlay: toml::Value = toml::from_str(
796 r#"
797[components.http]
798response_timeout_ms = 2000
799pool_max_idle_per_host = 99
800"#,
801 )
802 .unwrap();
803
804 merge_toml_values(&mut base, &overlay);
805
806 let http = base
807 .get("components")
808 .and_then(|v| v.get("http"))
809 .expect("merged http table");
810 assert_eq!(
811 http.get("connect_timeout_ms").and_then(|v| v.as_integer()),
812 Some(1000)
813 );
814 assert_eq!(
815 http.get("response_timeout_ms").and_then(|v| v.as_integer()),
816 Some(2000)
817 );
818 assert_eq!(
819 http.get("pool_max_idle_per_host")
820 .and_then(|v| v.as_integer()),
821 Some(99)
822 );
823 }
824
825 #[test]
826 fn test_from_file_with_profile_merges_default_and_profile() {
827 let file = write_temp_config(
828 r#"
829[default]
830watch = false
831[default.components.http]
832connect_timeout_ms = 1000
833pool_max_idle_per_host = 50
834
835[prod]
836watch = true
837[prod.components.http]
838pool_max_idle_per_host = 200
839"#,
840 );
841
842 let cfg = CamelConfig::from_file_with_profile(file.path().to_str().unwrap(), Some("prod"))
843 .expect("config should load");
844
845 assert!(cfg.watch);
846 let http = cfg.components.raw.get("http").expect("http config");
847 assert_eq!(
848 http.get("connect_timeout_ms").and_then(|v| v.as_integer()),
849 Some(1000)
850 );
851 assert_eq!(
852 http.get("pool_max_idle_per_host")
853 .and_then(|v| v.as_integer()),
854 Some(200)
855 );
856 }
857
858 #[test]
859 fn test_from_file_with_profile_uses_profile_when_no_default() {
860 let file = write_temp_config(
861 r#"
862[dev]
863watch = true
864timeout_ms = 777
865"#,
866 );
867
868 let cfg = CamelConfig::from_file_with_profile(file.path().to_str().unwrap(), Some("dev"))
869 .expect("config should load");
870 assert!(cfg.watch);
871 assert_eq!(cfg.timeout_ms, 777);
872 }
873
874 #[test]
875 fn test_from_file_with_profile_unknown_profile_returns_error() {
876 let file = write_temp_config(
877 r#"
878[default]
879watch = false
880"#,
881 );
882
883 let err = CamelConfig::from_file_with_profile(file.path().to_str().unwrap(), Some("qa"))
884 .expect_err("should fail");
885 assert!(err.to_string().contains("Unknown profile: qa"));
886 }
887
888 #[test]
889 fn test_from_file_without_profile_uses_default_section() {
890 let file = write_temp_config(
891 r#"
892[default]
893watch = true
894timeout_ms = 321
895"#,
896 );
897
898 let cfg =
899 CamelConfig::from_file(file.path().to_str().unwrap()).expect("config should load");
900 assert!(cfg.watch);
901 assert_eq!(cfg.timeout_ms, 321);
902 }
903
904 #[test]
905 fn test_from_file_with_env_overrides_timeout() {
906 let file = write_temp_config(
907 r#"
908[default]
909timeout_ms = 1000
910"#,
911 );
912
913 unsafe {
915 std::env::set_var("CAMEL_TIMEOUT_MS", "9999");
916 }
917
918 let cfg = CamelConfig::from_file_with_env(file.path().to_str().unwrap())
919 .expect("config should load with env override");
920 assert_eq!(cfg.timeout_ms, 9999);
921
922 unsafe {
924 std::env::remove_var("CAMEL_TIMEOUT_MS");
925 }
926 }
927}
928
929#[cfg(test)]
930mod additional_config_tests {
931 use super::*;
932
933 #[test]
934 fn journal_durability_converts_to_core_type() {
935 let immediate: camel_core::JournalDurability = JournalDurability::Immediate.into();
936 let eventual: camel_core::JournalDurability = JournalDurability::Eventual.into();
937 assert_eq!(immediate, camel_core::JournalDurability::Immediate);
938 assert_eq!(eventual, camel_core::JournalDurability::Eventual);
939 }
940
941 #[test]
942 fn supervision_into_supervision_config_converts_durations() {
943 let input = SupervisionCamelConfig {
944 max_attempts: Some(7),
945 initial_delay_ms: 123,
946 backoff_multiplier: 1.5,
947 max_delay_ms: 999,
948 };
949
950 let out = input.into_supervision_config();
951 assert_eq!(out.max_attempts, Some(7));
952 assert_eq!(out.initial_delay, Duration::from_millis(123));
953 assert_eq!(out.backoff_multiplier, 1.5);
954 assert_eq!(out.max_delay, Duration::from_millis(999));
955 }
956
957 #[test]
958 fn redb_journal_options_from_journal_config_copies_fields() {
959 let cfg = JournalConfig {
960 path: std::path::PathBuf::from("journal.db"),
961 durability: JournalDurability::Eventual,
962 compaction_threshold_events: 42,
963 };
964
965 let options: camel_core::RedbJournalOptions = (&cfg).into();
966 assert_eq!(options.durability, camel_core::JournalDurability::Eventual);
967 assert_eq!(options.compaction_threshold_events, 42);
968 }
969
970 #[test]
971 fn from_env_or_default_uses_camel_config_file_env() {
972 use std::io::Write;
973
974 let mut file = tempfile::NamedTempFile::new().unwrap();
975 file.write_all(
976 br#"
977watch = true
978timeout_ms = 111
979"#,
980 )
981 .unwrap();
982
983 unsafe {
984 std::env::set_var("CAMEL_CONFIG_FILE", file.path());
985 }
986 let cfg = CamelConfig::from_env_or_default().unwrap();
987 unsafe {
988 std::env::remove_var("CAMEL_CONFIG_FILE");
989 }
990
991 assert!(cfg.watch);
992 assert_eq!(cfg.timeout_ms, 111);
993 }
994
995 #[test]
996 fn from_file_with_profile_and_env_unknown_profile_errors() {
997 use std::io::Write;
998
999 let mut file = tempfile::NamedTempFile::new().unwrap();
1000 file.write_all(
1001 br#"
1002[default]
1003watch = false
1004"#,
1005 )
1006 .unwrap();
1007
1008 let err = CamelConfig::from_file_with_profile_and_env(
1009 file.path().to_str().unwrap(),
1010 Some("missing"),
1011 )
1012 .unwrap_err();
1013 assert!(err.to_string().contains("Unknown profile: missing"));
1014 }
1015}
1016
1017#[cfg(test)]
1018mod beans_config_tests {
1019 use super::*;
1020
1021 #[test]
1022 fn beans_default_empty() {
1023 let config: CamelConfig = toml::from_str("").unwrap();
1024 assert!(config.beans.is_empty());
1025 }
1026
1027 #[test]
1028 fn beans_parsed_from_config() {
1029 let toml_str = r#"
1030[beans.auth]
1031plugin = "my-auth"
1032
1033[beans.cache]
1034plugin = "my-cache"
1035"#;
1036 let config: CamelConfig = toml::from_str(toml_str).unwrap();
1037 assert_eq!(config.beans.len(), 2);
1038 assert_eq!(config.beans.get("auth").unwrap().plugin, "my-auth");
1039 assert_eq!(config.beans.get("cache").unwrap().plugin, "my-cache");
1040 }
1041}