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/// Authentication configuration.
298#[derive(Debug, Clone, Serialize, Deserialize, Default)]
299pub struct AuthConfig {
300    /// JWT secret for token validation.
301    pub jwt_secret: Option<String>,
302
303    /// Session TTL in seconds.
304    #[serde(default = "default_session_ttl")]
305    pub session_ttl_secs: u64,
306}
307
308fn default_session_ttl() -> u64 {
309    7 * 24 * 60 * 60 // 7 days
310}
311
312/// Substitute environment variables in the format ${VAR_NAME}.
313fn 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}