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
84fn 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 merge_toml_values(base_value, value);
93 } else {
94 base_table.insert(key.clone(), value.clone());
96 }
97 }
98 }
99 (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 let env_profile = env::var("CAMEL_PROFILE").ok();
118 let profile = profile.or_else(|| env_profile.as_deref());
119
120 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 let Some(p) = profile {
128 let default_value = config_value.get("default").cloned();
130
131 let profile_value = config_value.get(p).cloned();
133
134 if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
135 merge_toml_values(&mut base, &overlay);
137
138 config_value = base;
140 } else if let Some(profile_val) = config_value.get(p).cloned() {
141 config_value = profile_val;
143 } else {
144 return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
145 }
146 } else {
147 if let Some(default_val) = config_value.get("default").cloned() {
149 config_value = default_val;
150 }
151 }
152
153 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 let env_profile = env::var("CAMEL_PROFILE").ok();
174 let profile = profile.or_else(|| env_profile.as_deref());
175
176 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 let Some(p) = profile {
184 let default_value = config_value.get("default").cloned();
186
187 let profile_value = config_value.get(p).cloned();
189
190 if let (Some(mut base), Some(overlay)) = (default_value, profile_value) {
191 merge_toml_values(&mut base, &overlay);
193
194 config_value = base;
196 } else if let Some(profile_val) = config_value.get(p).cloned() {
197 config_value = profile_val;
199 } else {
200 return Err(ConfigError::Message(format!("Unknown profile: {}", p)));
201 }
202 } else {
203 if let Some(default_val) = config_value.get("default").cloned() {
205 config_value = default_val;
206 }
207 }
208
209 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}