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