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}