1mod cluster;
2mod database;
3mod observability;
4
5pub use cluster::ClusterConfig;
6pub use database::DatabaseConfig;
7pub use observability::ObservabilityConfig;
8
9use serde::{Deserialize, Serialize};
10use std::path::Path;
11
12use crate::error::{ForgeError, Result};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ForgeConfig {
17 #[serde(default)]
19 pub project: ProjectConfig,
20
21 pub database: DatabaseConfig,
23
24 #[serde(default)]
26 pub node: NodeConfig,
27
28 #[serde(default)]
30 pub gateway: GatewayConfig,
31
32 #[serde(default)]
34 pub function: FunctionConfig,
35
36 #[serde(default)]
38 pub worker: WorkerConfig,
39
40 #[serde(default)]
42 pub cluster: ClusterConfig,
43
44 #[serde(default)]
46 pub observability: ObservabilityConfig,
47
48 #[serde(default)]
50 pub security: SecurityConfig,
51}
52
53impl ForgeConfig {
54 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
56 let content = std::fs::read_to_string(path.as_ref())
57 .map_err(|e| ForgeError::Config(format!("Failed to read config file: {}", e)))?;
58
59 Self::parse_toml(&content)
60 }
61
62 pub fn parse_toml(content: &str) -> Result<Self> {
64 let content = substitute_env_vars(content);
66
67 toml::from_str(&content)
68 .map_err(|e| ForgeError::Config(format!("Failed to parse config: {}", e)))
69 }
70
71 pub fn default_with_database_url(url: &str) -> Self {
73 Self {
74 project: ProjectConfig::default(),
75 database: DatabaseConfig {
76 url: url.to_string(),
77 ..Default::default()
78 },
79 node: NodeConfig::default(),
80 gateway: GatewayConfig::default(),
81 function: FunctionConfig::default(),
82 worker: WorkerConfig::default(),
83 cluster: ClusterConfig::default(),
84 observability: ObservabilityConfig::default(),
85 security: SecurityConfig::default(),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ProjectConfig {
93 #[serde(default = "default_project_name")]
95 pub name: String,
96
97 #[serde(default = "default_version")]
99 pub version: String,
100}
101
102impl Default for ProjectConfig {
103 fn default() -> Self {
104 Self {
105 name: default_project_name(),
106 version: default_version(),
107 }
108 }
109}
110
111fn default_project_name() -> String {
112 "forge-app".to_string()
113}
114
115fn default_version() -> String {
116 "0.1.0".to_string()
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct NodeConfig {
122 #[serde(default = "default_roles")]
124 pub roles: Vec<NodeRole>,
125
126 #[serde(default = "default_capabilities")]
128 pub worker_capabilities: Vec<String>,
129}
130
131impl Default for NodeConfig {
132 fn default() -> Self {
133 Self {
134 roles: default_roles(),
135 worker_capabilities: default_capabilities(),
136 }
137 }
138}
139
140fn default_roles() -> Vec<NodeRole> {
141 vec![
142 NodeRole::Gateway,
143 NodeRole::Function,
144 NodeRole::Worker,
145 NodeRole::Scheduler,
146 ]
147}
148
149fn default_capabilities() -> Vec<String> {
150 vec!["general".to_string()]
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
155#[serde(rename_all = "lowercase")]
156pub enum NodeRole {
157 Gateway,
158 Function,
159 Worker,
160 Scheduler,
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct GatewayConfig {
166 #[serde(default = "default_http_port")]
168 pub port: u16,
169
170 #[serde(default = "default_grpc_port")]
172 pub grpc_port: u16,
173
174 #[serde(default = "default_max_connections")]
176 pub max_connections: usize,
177
178 #[serde(default = "default_request_timeout")]
180 pub request_timeout_secs: u64,
181}
182
183impl Default for GatewayConfig {
184 fn default() -> Self {
185 Self {
186 port: default_http_port(),
187 grpc_port: default_grpc_port(),
188 max_connections: default_max_connections(),
189 request_timeout_secs: default_request_timeout(),
190 }
191 }
192}
193
194fn default_http_port() -> u16 {
195 8080
196}
197
198fn default_grpc_port() -> u16 {
199 9000
200}
201
202fn default_max_connections() -> usize {
203 10000
204}
205
206fn default_request_timeout() -> u64 {
207 30
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct FunctionConfig {
213 #[serde(default = "default_max_concurrent")]
215 pub max_concurrent: usize,
216
217 #[serde(default = "default_function_timeout")]
219 pub timeout_secs: u64,
220
221 #[serde(default = "default_memory_limit")]
223 pub memory_limit: usize,
224}
225
226impl Default for FunctionConfig {
227 fn default() -> Self {
228 Self {
229 max_concurrent: default_max_concurrent(),
230 timeout_secs: default_function_timeout(),
231 memory_limit: default_memory_limit(),
232 }
233 }
234}
235
236fn default_max_concurrent() -> usize {
237 1000
238}
239
240fn default_function_timeout() -> u64 {
241 30
242}
243
244fn default_memory_limit() -> usize {
245 512 * 1024 * 1024 }
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct WorkerConfig {
251 #[serde(default = "default_max_concurrent_jobs")]
253 pub max_concurrent_jobs: usize,
254
255 #[serde(default = "default_job_timeout")]
257 pub job_timeout_secs: u64,
258
259 #[serde(default = "default_poll_interval")]
261 pub poll_interval_ms: u64,
262}
263
264impl Default for WorkerConfig {
265 fn default() -> Self {
266 Self {
267 max_concurrent_jobs: default_max_concurrent_jobs(),
268 job_timeout_secs: default_job_timeout(),
269 poll_interval_ms: default_poll_interval(),
270 }
271 }
272}
273
274fn default_max_concurrent_jobs() -> usize {
275 50
276}
277
278fn default_job_timeout() -> u64 {
279 3600 }
281
282fn default_poll_interval() -> u64 {
283 100
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, Default)]
288pub struct SecurityConfig {
289 pub secret_key: Option<String>,
291
292 #[serde(default)]
294 pub auth: AuthConfig,
295}
296
297#[derive(Debug, Clone, Serialize, Deserialize, Default)]
299pub struct AuthConfig {
300 pub jwt_secret: Option<String>,
302
303 #[serde(default = "default_session_ttl")]
305 pub session_ttl_secs: u64,
306}
307
308fn default_session_ttl() -> u64 {
309 7 * 24 * 60 * 60 }
311
312fn substitute_env_vars(content: &str) -> String {
314 let mut result = content.to_string();
315 let re = regex_lite::Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)\}").unwrap();
316
317 for cap in re.captures_iter(content) {
318 let var_name = &cap[1];
319 if let Ok(value) = std::env::var(var_name) {
320 result = result.replace(&cap[0], &value);
321 }
322 }
323
324 result
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_default_config() {
333 let config = ForgeConfig::default_with_database_url("postgres://localhost/test");
334 assert_eq!(config.gateway.port, 8080);
335 assert_eq!(config.node.roles.len(), 4);
336 }
337
338 #[test]
339 fn test_parse_minimal_config() {
340 let toml = r#"
341 [database]
342 url = "postgres://localhost/myapp"
343 "#;
344
345 let config = ForgeConfig::parse_toml(toml).unwrap();
346 assert_eq!(config.database.url, "postgres://localhost/myapp");
347 assert_eq!(config.gateway.port, 8080);
348 }
349
350 #[test]
351 fn test_parse_full_config() {
352 let toml = r#"
353 [project]
354 name = "my-app"
355 version = "1.0.0"
356
357 [database]
358 url = "postgres://localhost/myapp"
359 pool_size = 100
360
361 [node]
362 roles = ["gateway", "worker"]
363 worker_capabilities = ["media", "general"]
364
365 [gateway]
366 port = 3000
367 grpc_port = 9001
368 "#;
369
370 let config = ForgeConfig::parse_toml(toml).unwrap();
371 assert_eq!(config.project.name, "my-app");
372 assert_eq!(config.database.pool_size, 100);
373 assert_eq!(config.node.roles.len(), 2);
374 assert_eq!(config.gateway.port, 3000);
375 }
376
377 #[test]
378 fn test_env_var_substitution() {
379 unsafe {
380 std::env::set_var("TEST_DB_URL", "postgres://test:test@localhost/test");
381 }
382
383 let toml = r#"
384 [database]
385 url = "${TEST_DB_URL}"
386 "#;
387
388 let config = ForgeConfig::parse_toml(toml).unwrap();
389 assert_eq!(config.database.url, "postgres://test:test@localhost/test");
390
391 unsafe {
392 std::env::remove_var("TEST_DB_URL");
393 }
394 }
395}