mecha10-cli-core 0.6.3

Mecha10 CLI core foundation — shared types, services, and utilities
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 from specific path
/// let config = ConfigService::load_from(&PathBuf::from("custom.json")).await?;
/// println!("Robot ID: {}", config.robot.id);
///
/// // Find config in current or parent directories
/// let config_path = ConfigService::find_config()?;
/// # Ok(())
/// # }
/// ```
pub struct ConfigService;

impl ConfigService {
    /// 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")
    }

    /// Resolve the config file to validate and validate it -- the single
    /// implementation shared by `mecha10 config validate` and the config-validation
    /// step in `mecha10 lint`, so neither re-implements this logic locally.
    ///
    /// * `path` is `None` (the default): finds the project's `mecha10.json` via
    ///   [`Self::find_config`] and validates it plus every node config it references
    ///   (via `mecha10_core::schema_validation::validate_project_config`).
    /// * `path` is `Some`: validates exactly that one file, auto-detecting whether
    ///   it's a project config or a node config (via
    ///   `mecha10_core::schema_validation::validate_config_file`) -- no recursion
    ///   into other files. This is the mode a git hook uses to check only the files
    ///   staged in a commit.
    ///
    /// # Returns
    ///
    /// The path that was validated, so callers can report it.
    ///
    /// # Errors
    ///
    /// Returns an error with the underlying schema-validation failure details if the
    /// target file doesn't exist or fails validation.
    pub fn resolve_and_validate(path: Option<&Path>) -> Result<PathBuf> {
        Self::resolve_and_validate_with_node_schema_resolver(path, None)
    }

    /// Same as [`Self::resolve_and_validate`], but lets a caller supply a
    /// [`mecha10_core::schema_validation::NodeSchemaResolver`] so each referenced
    /// node config validates against a node-type-specific schema instead of the
    /// generic envelope (LAB-1772).
    ///
    /// This crate can't construct a resolver itself -- doing so needs each node's
    /// concrete config struct, and `mecha10-cli-core` can't depend on
    /// `packages/nodes/*` (see `docs/validate-configs.md`) -- so it only accepts one
    /// as a parameter. `packages/cli` builds the actual resolver (via
    /// `mecha10-schema-registry`, which *can* depend on the node crates) and passes
    /// it in here.
    ///
    /// # Errors
    ///
    /// Returns an error with the underlying schema-validation failure details if the
    /// target file doesn't exist or fails validation.
    pub fn resolve_and_validate_with_node_schema_resolver(
        path: Option<&Path>,
        node_schema_resolver: Option<&mecha10_core::schema_validation::NodeSchemaResolver>,
    ) -> Result<PathBuf> {
        use mecha10_core::schema_validation::{
            validate_config_file_with_node_schema_resolver, validate_project_config_with_node_schema_resolver,
        };

        let target = match path {
            Some(p) => p.to_path_buf(),
            None => Self::find_config()?,
        };

        if !target.exists() {
            anyhow::bail!("Configuration file not found: {}", target.display());
        }

        let result = match path {
            Some(_) => validate_config_file_with_node_schema_resolver(&target, node_schema_resolver),
            None => validate_project_config_with_node_schema_resolver(&target, node_schema_resolver),
        };

        result.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", target.display(), e))?;

        Ok(target)
    }
}