Skip to main content

camel_config/
config.rs

1use camel_core::config::TracerConfig;
2use config::{Config, ConfigError};
3use serde::{Deserialize, Serialize};
4use std::env;
5use std::time::Duration;
6
7#[derive(Debug, Clone, Deserialize)]
8pub struct CamelConfig {
9    #[serde(default)]
10    pub routes: Vec<String>,
11
12    /// Enable file-watcher hot-reload. Defaults to false.
13    /// Can be overridden per profile in Camel.toml or via `--watch` / `--no-watch` CLI flags.
14    #[serde(default)]
15    pub watch: bool,
16
17    #[serde(default = "default_log_level")]
18    pub log_level: String,
19
20    #[serde(default = "default_timeout_ms")]
21    pub timeout_ms: u64,
22
23    #[serde(default)]
24    pub components: ComponentsConfig,
25
26    #[serde(default)]
27    pub observability: ObservabilityConfig,
28
29    #[serde(default)]
30    pub supervision: Option<SupervisionCamelConfig>,
31}
32
33#[derive(Debug, Clone, Deserialize, Default, PartialEq)]
34pub struct ComponentsConfig {
35    #[serde(default)]
36    pub timer: Option<TimerConfig>,
37
38    #[serde(default)]
39    pub http: Option<HttpConfig>,
40}
41
42#[derive(Debug, Clone, Deserialize, PartialEq)]
43pub struct TimerConfig {
44    #[serde(default = "default_timer_period")]
45    pub period: u64,
46}
47
48#[derive(Debug, Clone, Deserialize, PartialEq)]
49pub struct HttpConfig {
50    #[serde(default = "default_http_connect_timeout")]
51    pub connect_timeout_ms: u64,
52
53    #[serde(default = "default_http_max_connections")]
54    pub max_connections: usize,
55}
56
57#[derive(Debug, Clone, Deserialize, Default)]
58pub struct ObservabilityConfig {
59    #[serde(default)]
60    pub metrics_enabled: bool,
61
62    #[serde(default = "default_metrics_port")]
63    pub metrics_port: u16,
64
65    #[serde(default)]
66    pub tracer: TracerConfig,
67}
68
69#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
70pub struct SupervisionCamelConfig {
71    /// Maximum number of restart attempts. `None` means retry forever.
72    pub max_attempts: Option<u32>,
73
74    /// Delay before the first restart attempt in milliseconds.
75    #[serde(default = "default_initial_delay_ms")]
76    pub initial_delay_ms: u64,
77
78    /// Multiplier applied to the delay after each failed attempt.
79    #[serde(default = "default_backoff_multiplier")]
80    pub backoff_multiplier: f64,
81
82    /// Maximum delay cap between restart attempts in milliseconds.
83    #[serde(default = "default_max_delay_ms")]
84    pub max_delay_ms: u64,
85}
86
87impl Default for SupervisionCamelConfig {
88    fn default() -> Self {
89        Self {
90            max_attempts: Some(5),
91            initial_delay_ms: 1000,
92            backoff_multiplier: 2.0,
93            max_delay_ms: 60000,
94        }
95    }
96}
97
98impl SupervisionCamelConfig {
99    /// Convert to camel_api::SupervisionConfig
100    pub fn into_supervision_config(self) -> camel_api::SupervisionConfig {
101        camel_api::SupervisionConfig {
102            max_attempts: self.max_attempts,
103            initial_delay: Duration::from_millis(self.initial_delay_ms),
104            backoff_multiplier: self.backoff_multiplier,
105            max_delay: Duration::from_millis(self.max_delay_ms),
106        }
107    }
108}
109
110fn default_log_level() -> String {
111    "INFO".to_string()
112}
113fn default_timeout_ms() -> u64 {
114    5000
115}
116fn default_timer_period() -> u64 {
117    1000
118}
119fn default_http_connect_timeout() -> u64 {
120    5000
121}
122fn default_http_max_connections() -> usize {
123    100
124}
125fn default_metrics_port() -> u16 {
126    9090
127}
128
129fn default_initial_delay_ms() -> u64 {
130    1000
131}
132
133fn default_backoff_multiplier() -> f64 {
134    2.0
135}
136
137fn default_max_delay_ms() -> u64 {
138    60000
139}
140
141/// Deep merge two TOML values
142/// Tables are merged recursively, with overlay values taking precedence
143fn merge_toml_values(base: &mut toml::Value, overlay: &toml::Value) {
144    match (base, overlay) {
145        (toml::Value::Table(base_table), toml::Value::Table(overlay_table)) => {
146            for (key, value) in overlay_table {
147                if let Some(base_value) = base_table.get_mut(key) {
148                    // Both have this key - merge recursively
149                    merge_toml_values(base_value, value);
150                } else {
151                    // Only overlay has this key - insert it
152                    base_table.insert(key.clone(), value.clone());
153                }
154            }
155        }
156        // For non-table values, overlay replaces base entirely
157        (base, overlay) => {
158            *base = overlay.clone();
159        }
160    }
161}
162
163impl CamelConfig {
164    pub fn from_file(path: &str) -> Result<Self, ConfigError> {
165        Self::from_file_with_profile(path, None)
166    }
167
168    pub fn from_file_with_env(path: &str) -> Result<Self, ConfigError> {
169        Self::from_file_with_profile_and_env(path, None)
170    }
171
172    pub fn from_file_with_profile(path: &str, profile: Option<&str>) -> Result<Self, ConfigError> {
173        // Get profile from parameter or environment variable
174        let env_profile = env::var("CAMEL_PROFILE").ok();
175        let profile = profile.or(env_profile.as_deref());
176
177        // Read the TOML file as a generic value for deep merging
178        let content = std::fs::read_to_string(path)
179            .map_err(|e| ConfigError::Message(format!("Failed to read config file: {}", e)))?;
180        let mut config_value: toml::Value = toml::from_str(&content)
181            .map_err(|e| ConfigError::Message(format!("Failed to parse TOML: {}", e)))?;
182
183        // If a profile is specified, merge it with default
184        if let Some(p) = profile {
185            // Extract default config as base
186            let default_value = config_value.get("default").cloned();
187
188            // Extract profile config
189            let profile_value = config_value.get(p).cloned();
190
191            if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
192                // Deep merge profile onto default
193                merge_toml_values(&mut base, &overlay);
194
195                // Replace the entire config with the merged result
196                config_value = base;
197            } else if let Some(profile_val) = config_value.get(p).cloned() {
198                // No default, just use profile
199                config_value = profile_val;
200            } else {
201                return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
202            }
203        } else {
204            // No profile specified, use default section if it exists
205            if let Some(default_val) = config_value.get("default").cloned() {
206                config_value = default_val;
207            }
208        }
209
210        // Deserialize the merged config
211        let merged_toml = toml::to_string(&config_value).map_err(|e| {
212            ConfigError::Message(format!("Failed to serialize merged config: {}", e))
213        })?;
214
215        let config = Config::builder()
216            .add_source(config::File::from_str(
217                &merged_toml,
218                config::FileFormat::Toml,
219            ))
220            .build()?;
221
222        config.try_deserialize()
223    }
224
225    pub fn from_file_with_profile_and_env(
226        path: &str,
227        profile: Option<&str>,
228    ) -> Result<Self, ConfigError> {
229        // Get profile from parameter or environment variable
230        let env_profile = env::var("CAMEL_PROFILE").ok();
231        let profile = profile.or(env_profile.as_deref());
232
233        // Read the TOML file as a generic value for deep merging
234        let content = std::fs::read_to_string(path)
235            .map_err(|e| ConfigError::Message(format!("Failed to read config file: {}", e)))?;
236        let mut config_value: toml::Value = toml::from_str(&content)
237            .map_err(|e| ConfigError::Message(format!("Failed to parse TOML: {}", e)))?;
238
239        // If a profile is specified, merge it with default
240        if let Some(p) = profile {
241            // Extract default config as base
242            let default_value = config_value.get("default").cloned();
243
244            // Extract profile config
245            let profile_value = config_value.get(p).cloned();
246
247            if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
248                // Deep merge profile onto default
249                merge_toml_values(&mut base, &overlay);
250
251                // Replace the entire config with the merged result
252                config_value = base;
253            } else if let Some(profile_val) = config_value.get(p).cloned() {
254                // No default, just use profile
255                config_value = profile_val;
256            } else {
257                return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
258            }
259        } else {
260            // No profile specified, use default section if it exists
261            if let Some(default_val) = config_value.get("default").cloned() {
262                config_value = default_val;
263            }
264        }
265
266        // Deserialize the merged config and apply environment variables
267        let merged_toml = toml::to_string(&config_value).map_err(|e| {
268            ConfigError::Message(format!("Failed to serialize merged config: {}", e))
269        })?;
270
271        let config = Config::builder()
272            .add_source(config::File::from_str(
273                &merged_toml,
274                config::FileFormat::Toml,
275            ))
276            .add_source(config::Environment::with_prefix("CAMEL").try_parsing(true))
277            .build()?;
278
279        config.try_deserialize()
280    }
281
282    pub fn from_env_or_default() -> Result<Self, ConfigError> {
283        let path = env::var("CAMEL_CONFIG_FILE").unwrap_or_else(|_| "Camel.toml".to_string());
284
285        Self::from_file(&path)
286    }
287}