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, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
299#[serde(rename_all = "UPPERCASE")]
300pub enum JwtAlgorithm {
301 #[default]
303 HS256,
304 HS384,
306 HS512,
308 RS256,
310 RS384,
312 RS512,
314}
315
316#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct AuthConfig {
319 #[serde(default)]
323 pub algorithm: JwtAlgorithm,
324
325 pub jwt_secret: Option<String>,
328
329 pub jwks_url: Option<String>,
332
333 pub issuer: Option<String>,
336
337 pub audience: Option<String>,
340
341 #[serde(default = "default_true")]
343 pub allow_anonymous: bool,
344
345 #[serde(default = "default_jwks_cache_ttl")]
347 pub jwks_cache_ttl_secs: u64,
348
349 #[serde(default = "default_session_ttl")]
351 pub session_ttl_secs: u64,
352}
353
354impl Default for AuthConfig {
355 fn default() -> Self {
356 Self {
357 algorithm: JwtAlgorithm::default(),
358 jwt_secret: None,
359 jwks_url: None,
360 issuer: None,
361 audience: None,
362 allow_anonymous: true,
363 jwks_cache_ttl_secs: default_jwks_cache_ttl(),
364 session_ttl_secs: default_session_ttl(),
365 }
366 }
367}
368
369impl AuthConfig {
370 pub fn validate(&self) -> Result<()> {
372 match self.algorithm {
373 JwtAlgorithm::HS256 | JwtAlgorithm::HS384 | JwtAlgorithm::HS512 => {
374 if self.jwt_secret.is_none() {
375 return Err(ForgeError::Config(
376 "jwt_secret is required for HMAC algorithms (HS256, HS384, HS512)".into(),
377 ));
378 }
379 }
380 JwtAlgorithm::RS256 | JwtAlgorithm::RS384 | JwtAlgorithm::RS512 => {
381 if self.jwks_url.is_none() {
382 return Err(ForgeError::Config(
383 "jwks_url is required for RSA algorithms (RS256, RS384, RS512)".into(),
384 ));
385 }
386 }
387 }
388 Ok(())
389 }
390
391 pub fn is_hmac(&self) -> bool {
393 matches!(
394 self.algorithm,
395 JwtAlgorithm::HS256 | JwtAlgorithm::HS384 | JwtAlgorithm::HS512
396 )
397 }
398
399 pub fn is_rsa(&self) -> bool {
401 matches!(
402 self.algorithm,
403 JwtAlgorithm::RS256 | JwtAlgorithm::RS384 | JwtAlgorithm::RS512
404 )
405 }
406}
407
408fn default_true() -> bool {
409 true
410}
411
412fn default_jwks_cache_ttl() -> u64 {
413 3600 }
415
416fn default_session_ttl() -> u64 {
417 7 * 24 * 60 * 60 }
419
420fn substitute_env_vars(content: &str) -> String {
422 let mut result = content.to_string();
423 let re = regex_lite::Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)\}").unwrap();
424
425 for cap in re.captures_iter(content) {
426 let var_name = &cap[1];
427 if let Ok(value) = std::env::var(var_name) {
428 result = result.replace(&cap[0], &value);
429 }
430 }
431
432 result
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn test_default_config() {
441 let config = ForgeConfig::default_with_database_url("postgres://localhost/test");
442 assert_eq!(config.gateway.port, 8080);
443 assert_eq!(config.node.roles.len(), 4);
444 }
445
446 #[test]
447 fn test_parse_minimal_config() {
448 let toml = r#"
449 [database]
450 url = "postgres://localhost/myapp"
451 "#;
452
453 let config = ForgeConfig::parse_toml(toml).unwrap();
454 assert_eq!(config.database.url, "postgres://localhost/myapp");
455 assert_eq!(config.gateway.port, 8080);
456 }
457
458 #[test]
459 fn test_parse_full_config() {
460 let toml = r#"
461 [project]
462 name = "my-app"
463 version = "1.0.0"
464
465 [database]
466 url = "postgres://localhost/myapp"
467 pool_size = 100
468
469 [node]
470 roles = ["gateway", "worker"]
471 worker_capabilities = ["media", "general"]
472
473 [gateway]
474 port = 3000
475 grpc_port = 9001
476 "#;
477
478 let config = ForgeConfig::parse_toml(toml).unwrap();
479 assert_eq!(config.project.name, "my-app");
480 assert_eq!(config.database.pool_size, 100);
481 assert_eq!(config.node.roles.len(), 2);
482 assert_eq!(config.gateway.port, 3000);
483 }
484
485 #[test]
486 fn test_env_var_substitution() {
487 unsafe {
488 std::env::set_var("TEST_DB_URL", "postgres://test:test@localhost/test");
489 }
490
491 let toml = r#"
492 [database]
493 url = "${TEST_DB_URL}"
494 "#;
495
496 let config = ForgeConfig::parse_toml(toml).unwrap();
497 assert_eq!(config.database.url, "postgres://test:test@localhost/test");
498
499 unsafe {
500 std::env::remove_var("TEST_DB_URL");
501 }
502 }
503}