mecha10-cli-core 0.9.0

Mecha10 CLI core foundation — shared types, services, and utilities
Documentation
#![allow(dead_code)]

//! Configuration service for managing project configuration
//!
//! Centralized interface for loading, validating, and working with mecha10.json files.

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

pub struct ConfigService;

impl ConfigService {
    /// Load project configuration from a specific mecha10.json path.
    ///
    /// Errors if the file is missing, unreadable, or fails to parse/validate.
    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 by searching upward from the current working directory.
    pub fn find_config() -> Result<PathBuf> {
        Self::find_config_from(&std::env::current_dir()?)
    }

    /// Find mecha10.json by searching upward from `start_dir`.
    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);
            }

            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 `dir` contains a mecha10.json
    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 just the robot ID from the mecha10.json at `path`.
    pub async fn load_robot_id(path: &Path) -> Result<String> {
        let config = Self::load_from(path).await?;
        Ok(config.robot.id)
    }

    /// Validate the configuration file at `path` against the mecha10-core schema.
    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 -- shared by `mecha10 config validate`
    /// and `mecha10 lint`. `None` validates the project's `mecha10.json` plus every node config
    /// it references; `Some` validates exactly that one file.
    pub fn resolve_and_validate(path: Option<&Path>) -> Result<PathBuf> {
        use mecha10_core::schema_validation::{validate_config_file, validate_project_config};

        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(&target),
            None => validate_project_config(&target),
        };

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

        Ok(target)
    }
}