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