mecha10-core 0.8.0

Mecha10 core runtime and foundational types
Documentation
//! Infrastructure configuration loading (Redis URLs, control plane, mecha10.json).

use super::{Context, InfrastructureConfig};
use crate::error::{Mecha10Error, Result};
use anyhow::Context as _;

impl Context {
    /// Load infrastructure configuration based on environment
    ///
    /// Priority order:
    /// 1. mecha10.json (project configuration) - redis.url field
    /// 2. Environment variables (MECHA10_REDIS_URL or REDIS_URL)
    /// 3. config/infrastructure.<environment>.yaml
    ///
    /// Set via MECHA10_ENVIRONMENT environment variable.
    pub(super) fn load_infrastructure_config() -> Result<InfrastructureConfig> {
        use crate::config::load_config;
        use std::env;
        use std::path::PathBuf;

        if let Ok(redis_url) = Self::try_load_redis_from_mecha10_json() {
            return Ok(InfrastructureConfig {
                redis: crate::context::infra_config::RedisConfig {
                    url: redis_url,
                    max_connections: 10,
                    connection_timeout_ms: 5000,
                },
                control_plane_redis: None,
                bridge: None,
                postgres: None,
                mlflow: None,
                minio: None,
            });
        }

        if let Ok(redis_url) = env::var("MECHA10_REDIS_URL").or_else(|_| env::var("REDIS_URL")) {
            return Ok(InfrastructureConfig {
                redis: crate::context::infra_config::RedisConfig {
                    url: redis_url,
                    max_connections: 10,
                    connection_timeout_ms: 5000,
                },
                control_plane_redis: None,
                bridge: None,
                postgres: None,
                mlflow: None,
                minio: None,
            });
        }

        let environment = env::var("MECHA10_ENVIRONMENT").unwrap_or_else(|_| "development".to_string());

        let config_filename = format!("infrastructure.{}.yaml", environment);

        let paths_to_try = vec![
            PathBuf::from(format!("config/{}", config_filename)),
            PathBuf::from(format!("../../config/{}", config_filename)),
            PathBuf::from(format!("../../../../config/{}", config_filename)),
        ];

        for path in &paths_to_try {
            if path.exists() {
                return load_config(path)
                    .with_context(|| {
                        format!(
                            "Failed to load infrastructure config for environment '{}' from '{}'",
                            environment,
                            path.display()
                        )
                    })
                    .map_err(|e| Mecha10Error::Configuration(format!("{:#}", e)));
            }
        }

        Err(Mecha10Error::Configuration(format!(
            "Failed to load infrastructure config for environment '{}'. \
             Tried paths: {}. \
             Make sure the file exists, set MECHA10_REDIS_URL environment variable, \
             or configure redis.url in mecha10.json.",
            environment,
            paths_to_try
                .iter()
                .map(|p| p.display().to_string())
                .collect::<Vec<_>>()
                .join(", ")
        )))
    }

    /// Try to load Redis URL from mecha10.json
    ///
    /// Walks up from the current directory (unbounded - see
    /// `crate::fs_utils::find_project_root`) looking for a mecha10.json and reads its
    /// redis.url field.
    fn try_load_redis_from_mecha10_json() -> Result<String> {
        use std::env;

        let current_dir = env::current_dir()
            .map_err(|e| Mecha10Error::Configuration(format!("Failed to get current directory: {}", e)))?;

        let project_root = crate::fs_utils::find_project_root(&current_dir).ok_or_else(|| {
            Mecha10Error::Configuration("No mecha10.json found with redis.url configuration".to_string())
        })?;

        let path = project_root.join(crate::fs_utils::PROJECT_CONFIG_FILE);

        let content = std::fs::read_to_string(&path)
            .map_err(|e| Mecha10Error::Configuration(format!("Failed to read {}: {}", path.display(), e)))?;

        let json: serde_json::Value = serde_json::from_str(&content)
            .map_err(|e| Mecha10Error::Configuration(format!("Failed to parse {}: {}", path.display(), e)))?;

        json.get("redis")
            .and_then(|r| r.get("url"))
            .and_then(|u| u.as_str())
            .map(|s| s.to_string())
            .ok_or_else(|| {
                Mecha10Error::Configuration("No mecha10.json found with redis.url configuration".to_string())
            })
    }

    /// Get Redis URL from infrastructure configuration (helper for internal use)
    pub(super) fn get_redis_url_from_config(config: &InfrastructureConfig) -> Result<String> {
        if let Ok(url) = std::env::var("MECHA10_REDIS_URL") {
            return Ok(url);
        }
        if let Ok(url) = std::env::var("REDIS_URL") {
            return Ok(url);
        }

        Ok(config.redis.url.clone())
    }

    /// Get Redis URL from infrastructure configuration
    pub fn get_redis_url() -> Result<String> {
        let config = Self::load_infrastructure_config()?;
        Self::get_redis_url_from_config(&config)
    }

    /// Get control plane Redis URL from infrastructure configuration
    pub fn get_control_plane_redis_url() -> Result<Option<String>> {
        let config = Self::load_infrastructure_config()?;
        Ok(config.control_plane_redis.map(|c| c.url))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serial_test::serial;
    use std::fs;

    #[test]
    #[serial]
    fn try_load_redis_from_mecha10_json_finds_config_several_levels_up() {
        let original_dir = std::env::current_dir().unwrap();

        let project = tempfile::tempdir().unwrap();
        fs::write(
            project.path().join("mecha10.json"),
            r#"{"redis": {"url": "redis://nested-project:6379"}}"#,
        )
        .unwrap();

        let nested = project.path().join("a").join("b").join("c");
        fs::create_dir_all(&nested).unwrap();
        std::env::set_current_dir(&nested).unwrap();

        let result = Context::try_load_redis_from_mecha10_json();

        std::env::set_current_dir(original_dir).unwrap();

        assert_eq!(result.unwrap(), "redis://nested-project:6379");
    }

    #[test]
    #[serial]
    fn try_load_redis_from_mecha10_json_errs_when_not_found() {
        let original_dir = std::env::current_dir().unwrap();

        let dir = tempfile::tempdir().unwrap();
        std::env::set_current_dir(dir.path()).unwrap();

        let result = Context::try_load_redis_from_mecha10_json();

        std::env::set_current_dir(original_dir).unwrap();

        assert!(result.is_err());
    }
}