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}