mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
#![allow(dead_code)]

//! Configuration service for managing project configuration
//!
//! This service provides a centralized interface for loading, validating,
//! and working with mecha10.json configuration files.

use crate::paths;
use crate::types::ProjectConfig;
use anyhow::{Context, Result};
use std::path::{Path, PathBuf};

/// Configuration service for project configuration management
///
/// # Examples
///
/// ```rust,ignore
/// use mecha10_cli::services::ConfigService;
/// use std::path::PathBuf;
///
/// # async fn example() -> anyhow::Result<()> {
/// // Load config from default location
/// let config = ConfigService::load_default().await?;
/// println!("Robot ID: {}", config.robot.id);
///
/// // Load from specific path
/// let config = ConfigService::load_from(&PathBuf::from("custom.json")).await?;
///
/// // Find config in current or parent directories
/// let config_path = ConfigService::find_config()?;
/// # Ok(())
/// # }
/// ```
pub struct ConfigService;

impl ConfigService {
    /// Load project configuration from the default location (mecha10.json)
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The file doesn't exist
    /// - The file cannot be read
    /// - The JSON is invalid
    /// - The configuration doesn't match the expected schema
    pub async fn load_default() -> Result<ProjectConfig> {
        Self::load_from(&PathBuf::from(paths::PROJECT_CONFIG)).await
    }

    /// Load project configuration from a specific path
    ///
    /// # Arguments
    ///
    /// * `path` - Path to the mecha10.json file
    ///
    /// # Errors
    ///
    /// Returns an error if:
    /// - The file doesn't exist
    /// - The file cannot be read
    /// - The JSON is invalid
    /// - The configuration doesn't match the expected schema
    pub async fn load_from(path: &Path) -> Result<ProjectConfig> {
        if !path.exists() {
            anyhow::bail!(
                "Project configuration not found at {}. Run 'mecha10 init' first.",
                path.display()
            );
        }

        let content = tokio::fs::read_to_string(path)
            .await
            .with_context(|| format!("Failed to read configuration file: {}", path.display()))?;

        let config: ProjectConfig = serde_json::from_str(&content)
            .with_context(|| format!("Failed to parse configuration file: {}", path.display()))?;

        Ok(config)
    }

    /// Find mecha10.json in the current directory or any parent directory
    ///
    /// Searches upward from the current working directory until it finds
    /// a mecha10.json file or reaches the root directory.
    ///
    /// # Returns
    ///
    /// Returns the path to the found configuration file.
    ///
    /// # Errors
    ///
    /// Returns an error if no mecha10.json file is found in the current
    /// directory or any parent directory.
    pub fn find_config() -> Result<PathBuf> {
        Self::find_config_from(&std::env::current_dir()?)
    }

    /// Find mecha10.json starting from a specific directory
    ///
    /// # Arguments
    ///
    /// * `start_dir` - Directory to start searching from
    ///
    /// # Returns
    ///
    /// Returns the path to the found configuration file.
    ///
    /// # Errors
    ///
    /// Returns an error if no mecha10.json file is found.
    pub fn find_config_from(start_dir: &Path) -> Result<PathBuf> {
        let mut current_dir = start_dir.to_path_buf();

        loop {
            let config_path = current_dir.join(paths::PROJECT_CONFIG);
            if config_path.exists() {
                return Ok(config_path);
            }

            // Try parent directory
            match current_dir.parent() {
                Some(parent) => current_dir = parent.to_path_buf(),
                None => {
                    anyhow::bail!(
                        "No mecha10.json found in {} or any parent directory.\n\n\
                         Run 'mecha10 init' to create a new project.",
                        start_dir.display()
                    )
                }
            }
        }
    }

    /// Check if a project is initialized in the given directory
    ///
    /// # Arguments
    ///
    /// * `dir` - Directory to check
    ///
    /// # Returns
    ///
    /// Returns `true` if mecha10.json exists in the directory.
    pub fn is_initialized(dir: &Path) -> bool {
        dir.join(paths::PROJECT_CONFIG).exists()
    }

    /// Check if a project is initialized in the current directory
    pub fn is_initialized_here() -> bool {
        PathBuf::from(paths::PROJECT_CONFIG).exists()
    }

    /// Load robot ID from configuration file
    ///
    /// This is a convenience method that loads just the robot ID
    /// without parsing the entire configuration.
    ///
    /// # Arguments
    ///
    /// * `path` - Path to the mecha10.json file
    ///
    /// # Errors
    ///
    /// Returns an error if the file cannot be read or parsed.
    pub async fn load_robot_id(path: &Path) -> Result<String> {
        let config = Self::load_from(path).await?;
        Ok(config.robot.id)
    }

    /// Validate configuration file
    ///
    /// Uses the mecha10-core schema validation to check if the configuration
    /// is valid according to the JSON schema and custom validation rules.
    ///
    /// # Arguments
    ///
    /// * `path` - Path to the configuration file
    ///
    /// # Errors
    ///
    /// Returns an error if validation fails with details about what's wrong.
    pub fn validate(path: &Path) -> Result<()> {
        use mecha10_core::schema_validation::validate_project_config;

        if !path.exists() {
            anyhow::bail!(
                "Configuration file not found: {}\n\nRun 'mecha10 init' to create a new project.",
                path.display()
            );
        }

        validate_project_config(path).context("Configuration validation failed")
    }

    /// Get the default config paths to try in order
    ///
    /// Returns a list of paths that are commonly used for configuration files.
    pub fn default_config_paths() -> Vec<PathBuf> {
        vec![
            PathBuf::from(paths::PROJECT_CONFIG),
            PathBuf::from(format!("config/{}", paths::PROJECT_CONFIG)),
            PathBuf::from(format!(".mecha10/{}", paths::PROJECT_CONFIG)),
        ]
    }

    /// Try to load configuration from any of the default paths
    ///
    /// Attempts to load configuration from each default path in order
    /// until one succeeds.
    ///
    /// # Errors
    ///
    /// Returns an error if none of the default paths contain a valid config.
    pub async fn try_load_from_defaults() -> Result<(PathBuf, ProjectConfig)> {
        let paths = Self::default_config_paths();

        for path in &paths {
            if path.exists() {
                match Self::load_from(path).await {
                    Ok(config) => return Ok((path.clone(), config)),
                    Err(_) => continue,
                }
            }
        }

        anyhow::bail!(
            "No valid mecha10.json found in default locations.\n\n\
             Tried:\n{}\n\n\
             Run 'mecha10 init' to create a new project.",
            paths
                .iter()
                .map(|p| format!("  - {}", p.display()))
                .collect::<Vec<_>>()
                .join("\n")
        )
    }
}