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 #[serde(default)]
54 pub auth: AuthConfig,
55}
56
57impl ForgeConfig {
58 pub fn from_file(path: impl AsRef<Path>) -> Result<Self> {
60 let content = std::fs::read_to_string(path.as_ref())
61 .map_err(|e| ForgeError::Config(format!("Failed to read config file: {}", e)))?;
62
63 Self::parse_toml(&content)
64 }
65
66 pub fn parse_toml(content: &str) -> Result<Self> {
68 let content = substitute_env_vars(content);
70
71 toml::from_str(&content)
72 .map_err(|e| ForgeError::Config(format!("Failed to parse config: {}", e)))
73 }
74
75 pub fn default_with_database_url(url: &str) -> Self {
77 Self {
78 project: ProjectConfig::default(),
79 database: DatabaseConfig {
80 url: url.to_string(),
81 ..Default::default()
82 },
83 node: NodeConfig::default(),
84 gateway: GatewayConfig::default(),
85 function: FunctionConfig::default(),
86 worker: WorkerConfig::default(),
87 cluster: ClusterConfig::default(),
88 observability: ObservabilityConfig::default(),
89 security: SecurityConfig::default(),
90 auth: AuthConfig::default(),
91 }
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct ProjectConfig {
98 #[serde(default = "default_project_name")]
100 pub name: String,
101
102 #[serde(default = "default_version")]
104 pub version: String,
105}
106
107impl Default for ProjectConfig {
108 fn default() -> Self {
109 Self {
110 name: default_project_name(),
111 version: default_version(),
112 }
113 }
114}
115
116fn default_project_name() -> String {
117 "forge-app".to_string()
118}
119
120fn default_version() -> String {
121 "0.1.0".to_string()
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct NodeConfig {
127 #[serde(default = "default_roles")]
129 pub roles: Vec<NodeRole>,
130
131 #[serde(default = "default_capabilities")]
133 pub worker_capabilities: Vec<String>,
134}
135
136impl Default for NodeConfig {
137 fn default() -> Self {
138 Self {
139 roles: default_roles(),
140 worker_capabilities: default_capabilities(),
141 }
142 }
143}
144
145fn default_roles() -> Vec<NodeRole> {
146 vec![
147 NodeRole::Gateway,
148 NodeRole::Function,
149 NodeRole::Worker,
150 NodeRole::Scheduler,
151 ]
152}
153
154fn default_capabilities() -> Vec<String> {
155 vec!["general".to_string()]
156}
157
158#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
160#[serde(rename_all = "lowercase")]
161pub enum NodeRole {
162 Gateway,
163 Function,
164 Worker,
165 Scheduler,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
170pub struct GatewayConfig {
171 #[serde(default = "default_http_port")]
173 pub port: u16,
174
175 #[serde(default = "default_grpc_port")]
177 pub grpc_port: u16,
178
179 #[serde(default = "default_max_connections")]
181 pub max_connections: usize,
182
183 #[serde(default = "default_request_timeout")]
185 pub request_timeout_secs: u64,
186}
187
188impl Default for GatewayConfig {
189 fn default() -> Self {
190 Self {
191 port: default_http_port(),
192 grpc_port: default_grpc_port(),
193 max_connections: default_max_connections(),
194 request_timeout_secs: default_request_timeout(),
195 }
196 }
197}
198
199fn default_http_port() -> u16 {
200 8080
201}
202
203fn default_grpc_port() -> u16 {
204 9000
205}
206
207fn default_max_connections() -> usize {
208 10000
209}
210
211fn default_request_timeout() -> u64 {
212 30
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct FunctionConfig {
218 #[serde(default = "default_max_concurrent")]
220 pub max_concurrent: usize,
221
222 #[serde(default = "default_function_timeout")]
224 pub timeout_secs: u64,
225
226 #[serde(default = "default_memory_limit")]
228 pub memory_limit: usize,
229}
230
231impl Default for FunctionConfig {
232 fn default() -> Self {
233 Self {
234 max_concurrent: default_max_concurrent(),
235 timeout_secs: default_function_timeout(),
236 memory_limit: default_memory_limit(),
237 }
238 }
239}
240
241fn default_max_concurrent() -> usize {
242 1000
243}
244
245fn default_function_timeout() -> u64 {
246 30
247}
248
249fn default_memory_limit() -> usize {
250 512 * 1024 * 1024 }
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub struct WorkerConfig {
256 #[serde(default = "default_max_concurrent_jobs")]
258 pub max_concurrent_jobs: usize,
259
260 #[serde(default = "default_job_timeout")]
262 pub job_timeout_secs: u64,
263
264 #[serde(default = "default_poll_interval")]
266 pub poll_interval_ms: u64,
267}
268
269impl Default for WorkerConfig {
270 fn default() -> Self {
271 Self {
272 max_concurrent_jobs: default_max_concurrent_jobs(),
273 job_timeout_secs: default_job_timeout(),
274 poll_interval_ms: default_poll_interval(),
275 }
276 }
277}
278
279fn default_max_concurrent_jobs() -> usize {
280 50
281}
282
283fn default_job_timeout() -> u64 {
284 3600 }
286
287fn default_poll_interval() -> u64 {
288 100
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, Default)]
293pub struct SecurityConfig {
294 pub secret_key: Option<String>,
296}
297
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
300#[serde(rename_all = "UPPERCASE")]
301pub enum JwtAlgorithm {
302 #[default]
304 HS256,
305 HS384,
307 HS512,
309 RS256,
311 RS384,
313 RS512,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
319pub struct AuthConfig {
320 pub jwt_secret: Option<String>,
323
324 #[serde(default)]
328 pub jwt_algorithm: JwtAlgorithm,
329
330 pub jwt_issuer: Option<String>,
333
334 pub jwt_audience: Option<String>,
337
338 pub token_expiry: Option<String>,
340
341 pub jwks_url: Option<String>,
344
345 #[serde(default = "default_true")]
347 pub allow_anonymous: bool,
348
349 #[serde(default = "default_jwks_cache_ttl")]
351 pub jwks_cache_ttl_secs: u64,
352
353 #[serde(default = "default_session_ttl")]
355 pub session_ttl_secs: u64,
356}
357
358impl Default for AuthConfig {
359 fn default() -> Self {
360 Self {
361 jwt_secret: None,
362 jwt_algorithm: JwtAlgorithm::default(),
363 jwt_issuer: None,
364 jwt_audience: None,
365 token_expiry: None,
366 jwks_url: None,
367 allow_anonymous: true,
368 jwks_cache_ttl_secs: default_jwks_cache_ttl(),
369 session_ttl_secs: default_session_ttl(),
370 }
371 }
372}
373
374impl AuthConfig {
375 pub fn validate(&self) -> Result<()> {
377 match self.jwt_algorithm {
378 JwtAlgorithm::HS256 | JwtAlgorithm::HS384 | JwtAlgorithm::HS512 => {
379 if self.jwt_secret.is_none() {
380 return Err(ForgeError::Config(
381 "jwt_secret is required for HMAC algorithms (HS256, HS384, HS512)".into(),
382 ));
383 }
384 }
385 JwtAlgorithm::RS256 | JwtAlgorithm::RS384 | JwtAlgorithm::RS512 => {
386 if self.jwks_url.is_none() {
387 return Err(ForgeError::Config(
388 "jwks_url is required for RSA algorithms (RS256, RS384, RS512)".into(),
389 ));
390 }
391 }
392 }
393 Ok(())
394 }
395
396 pub fn is_hmac(&self) -> bool {
398 matches!(
399 self.jwt_algorithm,
400 JwtAlgorithm::HS256 | JwtAlgorithm::HS384 | JwtAlgorithm::HS512
401 )
402 }
403
404 pub fn is_rsa(&self) -> bool {
406 matches!(
407 self.jwt_algorithm,
408 JwtAlgorithm::RS256 | JwtAlgorithm::RS384 | JwtAlgorithm::RS512
409 )
410 }
411}
412
413fn default_true() -> bool {
414 true
415}
416
417fn default_jwks_cache_ttl() -> u64 {
418 3600 }
420
421fn default_session_ttl() -> u64 {
422 7 * 24 * 60 * 60 }
424
425fn substitute_env_vars(content: &str) -> String {
427 let mut result = content.to_string();
428 let re = regex_lite::Regex::new(r"\$\{([A-Z_][A-Z0-9_]*)\}").unwrap();
429
430 for cap in re.captures_iter(content) {
431 let var_name = &cap[1];
432 if let Ok(value) = std::env::var(var_name) {
433 result = result.replace(&cap[0], &value);
434 }
435 }
436
437 result
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn test_default_config() {
446 let config = ForgeConfig::default_with_database_url("postgres://localhost/test");
447 assert_eq!(config.gateway.port, 8080);
448 assert_eq!(config.node.roles.len(), 4);
449 }
450
451 #[test]
452 fn test_parse_minimal_config() {
453 let toml = r#"
454 [database]
455 url = "postgres://localhost/myapp"
456 "#;
457
458 let config = ForgeConfig::parse_toml(toml).unwrap();
459 assert_eq!(config.database.url, "postgres://localhost/myapp");
460 assert_eq!(config.gateway.port, 8080);
461 }
462
463 #[test]
464 fn test_parse_full_config() {
465 let toml = r#"
466 [project]
467 name = "my-app"
468 version = "1.0.0"
469
470 [database]
471 url = "postgres://localhost/myapp"
472 pool_size = 100
473
474 [node]
475 roles = ["gateway", "worker"]
476 worker_capabilities = ["media", "general"]
477
478 [gateway]
479 port = 3000
480 grpc_port = 9001
481 "#;
482
483 let config = ForgeConfig::parse_toml(toml).unwrap();
484 assert_eq!(config.project.name, "my-app");
485 assert_eq!(config.database.pool_size, 100);
486 assert_eq!(config.node.roles.len(), 2);
487 assert_eq!(config.gateway.port, 3000);
488 }
489
490 #[test]
491 fn test_env_var_substitution() {
492 unsafe {
493 std::env::set_var("TEST_DB_URL", "postgres://test:test@localhost/test");
494 }
495
496 let toml = r#"
497 [database]
498 url = "${TEST_DB_URL}"
499 "#;
500
501 let config = ForgeConfig::parse_toml(toml).unwrap();
502 assert_eq!(config.database.url, "postgres://test:test@localhost/test");
503
504 unsafe {
505 std::env::remove_var("TEST_DB_URL");
506 }
507 }
508}