Skip to main content

camel_config/
config.rs

1use config::{Config, ConfigError};
2use serde::Deserialize;
3use std::env;
4
5#[derive(Debug, Clone, Deserialize)]
6pub struct CamelConfig {
7    #[serde(default)]
8    pub routes: Vec<String>,
9
10    #[serde(default = "default_log_level")]
11    pub log_level: String,
12
13    #[serde(default = "default_timeout_ms")]
14    pub timeout_ms: u64,
15
16    #[serde(default)]
17    pub components: ComponentsConfig,
18
19    #[serde(default)]
20    pub observability: ObservabilityConfig,
21}
22
23#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
24pub struct ComponentsConfig {
25    #[serde(default)]
26    pub timer: Option<TimerConfig>,
27
28    #[serde(default)]
29    pub http: Option<HttpConfig>,
30}
31
32#[derive(Debug, Clone, Deserialize, PartialEq)]
33pub struct TimerConfig {
34    #[serde(default = "default_timer_period")]
35    pub period: u64,
36}
37
38#[derive(Debug, Clone, Deserialize, PartialEq)]
39pub struct HttpConfig {
40    #[serde(default = "default_http_connect_timeout")]
41    pub connect_timeout_ms: u64,
42
43    #[serde(default = "default_http_max_connections")]
44    pub max_connections: usize,
45}
46
47#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
48pub struct ObservabilityConfig {
49    #[serde(default)]
50    pub metrics_enabled: bool,
51
52    #[serde(default = "default_metrics_port")]
53    pub metrics_port: u16,
54
55    #[serde(default)]
56    pub tracing_enabled: bool,
57
58    #[serde(default = "default_tracing_endpoint")]
59    pub tracing_endpoint: String,
60}
61
62fn default_log_level() -> String {
63    "INFO".to_string()
64}
65fn default_timeout_ms() -> u64 {
66    5000
67}
68fn default_timer_period() -> u64 {
69    1000
70}
71fn default_http_connect_timeout() -> u64 {
72    5000
73}
74fn default_http_max_connections() -> usize {
75    100
76}
77fn default_metrics_port() -> u16 {
78    9090
79}
80fn default_tracing_endpoint() -> String {
81    "http://localhost:4317".to_string()
82}
83
84/// Deep merge two TOML values
85/// Tables are merged recursively, with overlay values taking precedence
86fn merge_toml_values(base: &mut toml::Value, overlay: &toml::Value) {
87    match (base, overlay) {
88        (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
89            for (key, value) in overlay_table {
90                if let Some(base_value) = base_table.get_mut(key) {
91                    // Both have this key - merge recursively
92                    merge_toml_values(base_value, value);
93                } else {
94                    // Only overlay has this key - insert it
95                    base_table.insert(key.clone(), value.clone());
96                }
97            }
98        }
99        // For non-table values, overlay replaces base entirely
100        (base, overlay) => {
101            *base = overlay.clone();
102        }
103    }
104}
105
106impl CamelConfig {
107    pub fn from_file(path: &str) -> Result<Self, ConfigError> {
108        Self::from_file_with_profile(path, None)
109    }
110
111    pub fn from_file_with_env(path: &str) -> Result<Self, ConfigError> {
112        Self::from_file_with_profile_and_env(path, None)
113    }
114
115    pub fn from_file_with_profile(path: &str, profile: Option<&str>) -> Result<Self, ConfigError> {
116        // Get profile from parameter or environment variable
117        let env_profile = env::var("CAMEL_PROFILE").ok();
118        let profile = profile.or_else(|| env_profile.as_deref());
119
120        // Read the TOML file as a generic value for deep merging
121        let content = std::fs::read_to_string(path)
122            .map_err(|e| ConfigError::Message(format!("Failed to read config file: {}", e)))?;
123        let mut config_value: toml::Value = toml::from_str(&content)
124            .map_err(|e| ConfigError::Message(format!("Failed to parse TOML: {}", e)))?;
125
126        // If a profile is specified, merge it with default
127        if let Some(p) = profile {
128            // Extract default config as base
129            let default_value = config_value.get("default").cloned();
130
131            // Extract profile config
132            let profile_value = config_value.get(p).cloned();
133
134            if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
135                // Deep merge profile onto default
136                merge_toml_values(&mut base, &overlay);
137
138                // Replace the entire config with the merged result
139                config_value = base;
140            } else if let Some(profile_val) = config_value.get(p).cloned() {
141                // No default, just use profile
142                config_value = profile_val;
143            } else {
144                return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
145            }
146        } else {
147            // No profile specified, use default section if it exists
148            if let Some(default_val) = config_value.get("default").cloned() {
149                config_value = default_val;
150            }
151        }
152
153        // Deserialize the merged config
154        let merged_toml = toml::to_string(&config_value).map_err(|e| {
155            ConfigError::Message(format!("Failed to serialize merged config: {}", e))
156        })?;
157
158        let config = Config::builder()
159            .add_source(config::File::from_str(
160                &merged_toml,
161                config::FileFormat::Toml,
162            ))
163            .build()?;
164
165        config.try_deserialize()
166    }
167
168    pub fn from_file_with_profile_and_env(
169        path: &str,
170        profile: Option<&str>,
171    ) -> Result<Self, ConfigError> {
172        // Get profile from parameter or environment variable
173        let env_profile = env::var("CAMEL_PROFILE").ok();
174        let profile = profile.or_else(|| env_profile.as_deref());
175
176        // Read the TOML file as a generic value for deep merging
177        let content = std::fs::read_to_string(path)
178            .map_err(|e| ConfigError::Message(format!("Failed to read config file: {}", e)))?;
179        let mut config_value: toml::Value = toml::from_str(&content)
180            .map_err(|e| ConfigError::Message(format!("Failed to parse TOML: {}", e)))?;
181
182        // If a profile is specified, merge it with default
183        if let Some(p) = profile {
184            // Extract default config as base
185            let default_value = config_value.get("default").cloned();
186
187            // Extract profile config
188            let profile_value = config_value.get(p).cloned();
189
190            if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
191                // Deep merge profile onto default
192                merge_toml_values(&mut base, &overlay);
193
194                // Replace the entire config with the merged result
195                config_value = base;
196            } else if let Some(profile_val) = config_value.get(p).cloned() {
197                // No default, just use profile
198                config_value = profile_val;
199            } else {
200                return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
201            }
202        } else {
203            // No profile specified, use default section if it exists
204            if let Some(default_val) = config_value.get("default").cloned() {
205                config_value = default_val;
206            }
207        }
208
209        // Deserialize the merged config and apply environment variables
210        let merged_toml = toml::to_string(&config_value).map_err(|e| {
211            ConfigError::Message(format!("Failed to serialize merged config: {}", e))
212        })?;
213
214        let config = Config::builder()
215            .add_source(config::File::from_str(
216                &merged_toml,
217                config::FileFormat::Toml,
218            ))
219            .add_source(config::Environment::with_prefix("CAMEL").try_parsing(true))
220            .build()?;
221
222        config.try_deserialize()
223    }
224
225    pub fn from_env_or_default() -> Result<Self, ConfigError> {
226        let path = env::var("CAMEL_CONFIG_FILE").unwrap_or_else(|_| "Camel.toml".to_string());
227
228        Self::from_file(&path)
229    }
230}