forge_core/config/
mod.rs

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/// Root configuration for FORGE.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ForgeConfig {
17    /// Project metadata.
18    #[serde(default)]
19    pub project: ProjectConfig,
20
21    /// Database configuration.
22    pub database: DatabaseConfig,
23
24    /// Node configuration.
25    #[serde(default)]
26    pub node: NodeConfig,
27
28    /// Gateway configuration.
29    #[serde(default)]
30    pub gateway: GatewayConfig,
31
32    /// Function execution configuration.
33    #[serde(default)]
34    pub function: FunctionConfig,
35
36    /// Worker configuration.
37    #[serde(default)]
38    pub worker: WorkerConfig,
39
40    /// Cluster configuration.
41    #[serde(default)]
42    pub cluster: ClusterConfig,
43
44    /// Observability configuration.
45    #[serde(default)]
46    pub observability: ObservabilityConfig,
47
48    /// Security configuration.
49    #[serde(default)]
50    pub security: SecurityConfig,
51}
52
53impl ForgeConfig {
54    /// Load configuration from a TOML file.
55    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    /// Parse configuration from a TOML string.
63    pub fn parse_toml(content: &str) -> Result<Self> {
64        // Substitute environment variables
65        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    /// Load configuration with defaults.
72    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/// Project metadata.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ProjectConfig {
93    /// Project name.
94    #[serde(default = "default_project_name")]
95    pub name: String,
96
97    /// Project version.
98    #[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/// Node role configuration.
120#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct NodeConfig {
122    /// Roles this node should assume.
123    #[serde(default = "default_roles")]
124    pub roles: Vec<NodeRole>,
125
126    /// Worker capabilities for job routing.
127    #[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/// Available node roles.
154#[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/// Gateway configuration.
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct GatewayConfig {
166    /// HTTP port.
167    #[serde(default = "default_http_port")]
168    pub port: u16,
169
170    /// gRPC port for inter-node communication.
171    #[serde(default = "default_grpc_port")]
172    pub grpc_port: u16,
173
174    /// Maximum concurrent connections.
175    #[serde(default = "default_max_connections")]
176    pub max_connections: usize,
177
178    /// Request timeout in seconds.
179    #[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/// Function execution configuration.
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct FunctionConfig {
213    /// Maximum concurrent function executions.
214    #[serde(default = "default_max_concurrent")]
215    pub max_concurrent: usize,
216
217    /// Function timeout in seconds.
218    #[serde(default = "default_function_timeout")]
219    pub timeout_secs: u64,
220
221    /// Memory limit per function (in bytes).
222    #[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 // 512 MiB
246}
247
248/// Worker configuration.
249#[derive(Debug, Clone, Serialize, Deserialize)]
250pub struct WorkerConfig {
251    /// Maximum concurrent jobs.
252    #[serde(default = "default_max_concurrent_jobs")]
253    pub max_concurrent_jobs: usize,
254
255    /// Job timeout in seconds.
256    #[serde(default = "default_job_timeout")]
257    pub job_timeout_secs: u64,
258
259    /// Poll interval in milliseconds.
260    #[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 // 1 hour
280}
281
282fn default_poll_interval() -> u64 {
283    100
284}
285
286/// Security configuration.
287#[derive(Debug, Clone, Serialize, Deserialize, Default)]
288pub struct SecurityConfig {
289    /// Secret key for signing.
290    pub secret_key: Option<String>,
291
292    /// JWT configuration.
293    #[serde(default)]
294    pub auth: AuthConfig,
295}
296
297/// JWT signing algorithm.
298#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
299#[serde(rename_all = "UPPERCASE")]
300pub enum JwtAlgorithm {
301    /// HMAC using SHA-256 (symmetric, requires jwt_secret).
302    #[default]
303    HS256,
304    /// HMAC using SHA-384 (symmetric, requires jwt_secret).
305    HS384,
306    /// HMAC using SHA-512 (symmetric, requires jwt_secret).
307    HS512,
308    /// RSA using SHA-256 (asymmetric, requires jwks_url).
309    RS256,
310    /// RSA using SHA-384 (asymmetric, requires jwks_url).
311    RS384,
312    /// RSA using SHA-512 (asymmetric, requires jwks_url).
313    RS512,
314}
315
316/// Authentication configuration.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct AuthConfig {
319    /// JWT signing algorithm.
320    /// HMAC algorithms (HS256, HS384, HS512) require jwt_secret.
321    /// RSA algorithms (RS256, RS384, RS512) require jwks_url.
322    #[serde(default)]
323    pub algorithm: JwtAlgorithm,
324
325    /// JWT secret for HMAC algorithms (HS256, HS384, HS512).
326    /// Required when using HMAC algorithms.
327    pub jwt_secret: Option<String>,
328
329    /// JWKS URL for RSA algorithms (RS256, RS384, RS512).
330    /// Keys are fetched and cached automatically.
331    pub jwks_url: Option<String>,
332
333    /// Expected token issuer (iss claim).
334    /// If set, tokens with a different issuer are rejected.
335    pub issuer: Option<String>,
336
337    /// Expected audience (aud claim).
338    /// If set, tokens with a different audience are rejected.
339    pub audience: Option<String>,
340
341    /// Allow unauthenticated requests to reach public functions.
342    #[serde(default = "default_true")]
343    pub allow_anonymous: bool,
344
345    /// JWKS cache TTL in seconds.
346    #[serde(default = "default_jwks_cache_ttl")]
347    pub jwks_cache_ttl_secs: u64,
348
349    /// Session TTL in seconds (for WebSocket sessions).
350    #[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    /// Validate that the configuration is complete for the chosen algorithm.
371    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    /// Check if this config uses HMAC (symmetric) algorithms.
392    pub fn is_hmac(&self) -> bool {
393        matches!(
394            self.algorithm,
395            JwtAlgorithm::HS256 | JwtAlgorithm::HS384 | JwtAlgorithm::HS512
396        )
397    }
398
399    /// Check if this config uses RSA (asymmetric) algorithms.
400    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 // 1 hour
414}
415
416fn default_session_ttl() -> u64 {
417    7 * 24 * 60 * 60 // 7 days
418}
419
420/// Substitute environment variables in the format ${VAR_NAME}.
421fn 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}