Skip to main content

mecha10_core/context/
config.rs

1//! Infrastructure configuration loading (Redis URLs, control plane, mecha10.json).
2
3use super::{Context, InfrastructureConfig};
4use crate::error::{Mecha10Error, Result};
5use anyhow::Context as _;
6
7impl Context {
8    /// Load infrastructure configuration based on environment
9    ///
10    /// Priority order:
11    /// 1. mecha10.json (project configuration) - redis.url field
12    /// 2. Environment variables (MECHA10_REDIS_URL or REDIS_URL)
13    /// 3. config/infrastructure.<environment>.yaml
14    ///
15    /// Set via MECHA10_ENVIRONMENT environment variable.
16    pub(super) fn load_infrastructure_config() -> Result<InfrastructureConfig> {
17        use crate::config::load_config;
18        use std::env;
19        use std::path::PathBuf;
20
21        if let Ok(redis_url) = Self::try_load_redis_from_mecha10_json() {
22            return Ok(InfrastructureConfig {
23                redis: crate::context::infra_config::RedisConfig {
24                    url: redis_url,
25                    max_connections: 10,
26                    connection_timeout_ms: 5000,
27                },
28                control_plane_redis: None,
29                bridge: None,
30                postgres: None,
31                mlflow: None,
32                minio: None,
33            });
34        }
35
36        if let Ok(redis_url) = env::var("MECHA10_REDIS_URL").or_else(|_| env::var("REDIS_URL")) {
37            return Ok(InfrastructureConfig {
38                redis: crate::context::infra_config::RedisConfig {
39                    url: redis_url,
40                    max_connections: 10,
41                    connection_timeout_ms: 5000,
42                },
43                control_plane_redis: None,
44                bridge: None,
45                postgres: None,
46                mlflow: None,
47                minio: None,
48            });
49        }
50
51        let environment = env::var("MECHA10_ENVIRONMENT").unwrap_or_else(|_| "development".to_string());
52
53        let config_filename = format!("infrastructure.{}.yaml", environment);
54
55        let paths_to_try = vec![
56            PathBuf::from(format!("config/{}", config_filename)),
57            PathBuf::from(format!("../../config/{}", config_filename)),
58            PathBuf::from(format!("../../../../config/{}", config_filename)),
59        ];
60
61        for path in &paths_to_try {
62            if path.exists() {
63                return load_config(path)
64                    .with_context(|| {
65                        format!(
66                            "Failed to load infrastructure config for environment '{}' from '{}'",
67                            environment,
68                            path.display()
69                        )
70                    })
71                    .map_err(|e| Mecha10Error::Configuration(format!("{:#}", e)));
72            }
73        }
74
75        Err(Mecha10Error::Configuration(format!(
76            "Failed to load infrastructure config for environment '{}'. \
77             Tried paths: {}. \
78             Make sure the file exists, set MECHA10_REDIS_URL environment variable, \
79             or configure redis.url in mecha10.json.",
80            environment,
81            paths_to_try
82                .iter()
83                .map(|p| p.display().to_string())
84                .collect::<Vec<_>>()
85                .join(", ")
86        )))
87    }
88
89    /// Try to load Redis URL from mecha10.json
90    ///
91    /// Walks up from the current directory (unbounded - see
92    /// `crate::fs_utils::find_project_root`) looking for a mecha10.json and reads its
93    /// redis.url field.
94    fn try_load_redis_from_mecha10_json() -> Result<String> {
95        use std::env;
96
97        let current_dir = env::current_dir()
98            .map_err(|e| Mecha10Error::Configuration(format!("Failed to get current directory: {}", e)))?;
99
100        let project_root = crate::fs_utils::find_project_root(&current_dir).ok_or_else(|| {
101            Mecha10Error::Configuration("No mecha10.json found with redis.url configuration".to_string())
102        })?;
103
104        let path = project_root.join(crate::fs_utils::PROJECT_CONFIG_FILE);
105
106        let content = std::fs::read_to_string(&path)
107            .map_err(|e| Mecha10Error::Configuration(format!("Failed to read {}: {}", path.display(), e)))?;
108
109        let json: serde_json::Value = serde_json::from_str(&content)
110            .map_err(|e| Mecha10Error::Configuration(format!("Failed to parse {}: {}", path.display(), e)))?;
111
112        json.get("redis")
113            .and_then(|r| r.get("url"))
114            .and_then(|u| u.as_str())
115            .map(|s| s.to_string())
116            .ok_or_else(|| {
117                Mecha10Error::Configuration("No mecha10.json found with redis.url configuration".to_string())
118            })
119    }
120
121    /// Get Redis URL from infrastructure configuration (helper for internal use)
122    pub(super) fn get_redis_url_from_config(config: &InfrastructureConfig) -> Result<String> {
123        if let Ok(url) = std::env::var("MECHA10_REDIS_URL") {
124            return Ok(url);
125        }
126        if let Ok(url) = std::env::var("REDIS_URL") {
127            return Ok(url);
128        }
129
130        Ok(config.redis.url.clone())
131    }
132
133    /// Get Redis URL from infrastructure configuration
134    pub fn get_redis_url() -> Result<String> {
135        let config = Self::load_infrastructure_config()?;
136        Self::get_redis_url_from_config(&config)
137    }
138
139    /// Get control plane Redis URL from infrastructure configuration
140    pub fn get_control_plane_redis_url() -> Result<Option<String>> {
141        let config = Self::load_infrastructure_config()?;
142        Ok(config.control_plane_redis.map(|c| c.url))
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use serial_test::serial;
150    use std::fs;
151
152    #[test]
153    #[serial]
154    fn try_load_redis_from_mecha10_json_finds_config_several_levels_up() {
155        let original_dir = std::env::current_dir().unwrap();
156
157        let project = tempfile::tempdir().unwrap();
158        fs::write(
159            project.path().join("mecha10.json"),
160            r#"{"redis": {"url": "redis://nested-project:6379"}}"#,
161        )
162        .unwrap();
163
164        let nested = project.path().join("a").join("b").join("c");
165        fs::create_dir_all(&nested).unwrap();
166        std::env::set_current_dir(&nested).unwrap();
167
168        let result = Context::try_load_redis_from_mecha10_json();
169
170        std::env::set_current_dir(original_dir).unwrap();
171
172        assert_eq!(result.unwrap(), "redis://nested-project:6379");
173    }
174
175    #[test]
176    #[serial]
177    fn try_load_redis_from_mecha10_json_errs_when_not_found() {
178        let original_dir = std::env::current_dir().unwrap();
179
180        let dir = tempfile::tempdir().unwrap();
181        std::env::set_current_dir(dir.path()).unwrap();
182
183        let result = Context::try_load_redis_from_mecha10_json();
184
185        std::env::set_current_dir(original_dir).unwrap();
186
187        assert!(result.is_err());
188    }
189}