mecha10_cli/services/
config.rs

1#![allow(dead_code)]
2
3//! Configuration service for managing project configuration
4//!
5//! This service provides a centralized interface for loading, validating,
6//! and working with mecha10.json configuration files.
7
8use crate::types::ProjectConfig;
9use anyhow::{Context, Result};
10use std::path::{Path, PathBuf};
11
12/// Configuration service for project configuration management
13///
14/// # Examples
15///
16/// ```rust,ignore
17/// use mecha10_cli::services::ConfigService;
18/// use std::path::PathBuf;
19///
20/// # async fn example() -> anyhow::Result<()> {
21/// // Load config from default location
22/// let config = ConfigService::load_default().await?;
23/// println!("Robot ID: {}", config.robot.id);
24///
25/// // Load from specific path
26/// let config = ConfigService::load_from(&PathBuf::from("custom.json")).await?;
27///
28/// // Find config in current or parent directories
29/// let config_path = ConfigService::find_config()?;
30/// # Ok(())
31/// # }
32/// ```
33pub struct ConfigService;
34
35impl ConfigService {
36    /// Load project configuration from the default location (mecha10.json)
37    ///
38    /// # Errors
39    ///
40    /// Returns an error if:
41    /// - The file doesn't exist
42    /// - The file cannot be read
43    /// - The JSON is invalid
44    /// - The configuration doesn't match the expected schema
45    pub async fn load_default() -> Result<ProjectConfig> {
46        Self::load_from(&PathBuf::from("mecha10.json")).await
47    }
48
49    /// Load project configuration from a specific path
50    ///
51    /// # Arguments
52    ///
53    /// * `path` - Path to the mecha10.json file
54    ///
55    /// # Errors
56    ///
57    /// Returns an error if:
58    /// - The file doesn't exist
59    /// - The file cannot be read
60    /// - The JSON is invalid
61    /// - The configuration doesn't match the expected schema
62    pub async fn load_from(path: &Path) -> Result<ProjectConfig> {
63        if !path.exists() {
64            anyhow::bail!(
65                "Project configuration not found at {}. Run 'mecha10 init' first.",
66                path.display()
67            );
68        }
69
70        let content = tokio::fs::read_to_string(path)
71            .await
72            .with_context(|| format!("Failed to read configuration file: {}", path.display()))?;
73
74        let config: ProjectConfig = serde_json::from_str(&content)
75            .with_context(|| format!("Failed to parse configuration file: {}", path.display()))?;
76
77        Ok(config)
78    }
79
80    /// Find mecha10.json in the current directory or any parent directory
81    ///
82    /// Searches upward from the current working directory until it finds
83    /// a mecha10.json file or reaches the root directory.
84    ///
85    /// # Returns
86    ///
87    /// Returns the path to the found configuration file.
88    ///
89    /// # Errors
90    ///
91    /// Returns an error if no mecha10.json file is found in the current
92    /// directory or any parent directory.
93    pub fn find_config() -> Result<PathBuf> {
94        Self::find_config_from(&std::env::current_dir()?)
95    }
96
97    /// Find mecha10.json starting from a specific directory
98    ///
99    /// # Arguments
100    ///
101    /// * `start_dir` - Directory to start searching from
102    ///
103    /// # Returns
104    ///
105    /// Returns the path to the found configuration file.
106    ///
107    /// # Errors
108    ///
109    /// Returns an error if no mecha10.json file is found.
110    pub fn find_config_from(start_dir: &Path) -> Result<PathBuf> {
111        let mut current_dir = start_dir.to_path_buf();
112
113        loop {
114            let config_path = current_dir.join("mecha10.json");
115            if config_path.exists() {
116                return Ok(config_path);
117            }
118
119            // Try parent directory
120            match current_dir.parent() {
121                Some(parent) => current_dir = parent.to_path_buf(),
122                None => {
123                    anyhow::bail!(
124                        "No mecha10.json found in {} or any parent directory.\n\n\
125                         Run 'mecha10 init' to create a new project.",
126                        start_dir.display()
127                    )
128                }
129            }
130        }
131    }
132
133    /// Check if a project is initialized in the given directory
134    ///
135    /// # Arguments
136    ///
137    /// * `dir` - Directory to check
138    ///
139    /// # Returns
140    ///
141    /// Returns `true` if mecha10.json exists in the directory.
142    pub fn is_initialized(dir: &Path) -> bool {
143        dir.join("mecha10.json").exists()
144    }
145
146    /// Check if a project is initialized in the current directory
147    pub fn is_initialized_here() -> bool {
148        PathBuf::from("mecha10.json").exists()
149    }
150
151    /// Load robot ID from configuration file
152    ///
153    /// This is a convenience method that loads just the robot ID
154    /// without parsing the entire configuration.
155    ///
156    /// # Arguments
157    ///
158    /// * `path` - Path to the mecha10.json file
159    ///
160    /// # Errors
161    ///
162    /// Returns an error if the file cannot be read or parsed.
163    pub async fn load_robot_id(path: &Path) -> Result<String> {
164        let config = Self::load_from(path).await?;
165        Ok(config.robot.id)
166    }
167
168    /// Validate configuration file
169    ///
170    /// Uses the mecha10-core schema validation to check if the configuration
171    /// is valid according to the JSON schema and custom validation rules.
172    ///
173    /// # Arguments
174    ///
175    /// * `path` - Path to the configuration file
176    ///
177    /// # Errors
178    ///
179    /// Returns an error if validation fails with details about what's wrong.
180    pub fn validate(path: &Path) -> Result<()> {
181        use mecha10_core::schema_validation::validate_project_config;
182
183        if !path.exists() {
184            anyhow::bail!(
185                "Configuration file not found: {}\n\nRun 'mecha10 init' to create a new project.",
186                path.display()
187            );
188        }
189
190        validate_project_config(path).context("Configuration validation failed")
191    }
192
193    /// Get the default config paths to try in order
194    ///
195    /// Returns a list of paths that are commonly used for configuration files.
196    pub fn default_config_paths() -> Vec<PathBuf> {
197        vec![
198            PathBuf::from("mecha10.json"),
199            PathBuf::from("config/mecha10.json"),
200            PathBuf::from(".mecha10/mecha10.json"),
201        ]
202    }
203
204    /// Try to load configuration from any of the default paths
205    ///
206    /// Attempts to load configuration from each default path in order
207    /// until one succeeds.
208    ///
209    /// # Errors
210    ///
211    /// Returns an error if none of the default paths contain a valid config.
212    pub async fn try_load_from_defaults() -> Result<(PathBuf, ProjectConfig)> {
213        let paths = Self::default_config_paths();
214
215        for path in &paths {
216            if path.exists() {
217                match Self::load_from(path).await {
218                    Ok(config) => return Ok((path.clone(), config)),
219                    Err(_) => continue,
220                }
221            }
222        }
223
224        anyhow::bail!(
225            "No valid mecha10.json found in default locations.\n\n\
226             Tried:\n{}\n\n\
227             Run 'mecha10 init' to create a new project.",
228            paths
229                .iter()
230                .map(|p| format!("  - {}", p.display()))
231                .collect::<Vec<_>>()
232                .join("\n")
233        )
234    }
235}