mecha10_cli_core/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 from specific path
23/// let config = ConfigService::load_from(&PathBuf::from("custom.json")).await?;
24/// println!("Robot ID: {}", config.robot.id);
25///
26/// // Find config in current or parent directories
27/// let config_path = ConfigService::find_config()?;
28/// # Ok(())
29/// # }
30/// ```
31pub struct ConfigService;
32
33impl ConfigService {
34 /// Load project configuration from a specific path
35 ///
36 /// # Arguments
37 ///
38 /// * `path` - Path to the mecha10.json file
39 ///
40 /// # Errors
41 ///
42 /// Returns an error if:
43 /// - The file doesn't exist
44 /// - The file cannot be read
45 /// - The JSON is invalid
46 /// - The configuration doesn't match the expected schema
47 pub async fn load_from(path: &Path) -> Result<ProjectConfig> {
48 if !path.exists() {
49 anyhow::bail!(
50 "Project configuration not found at {}. Run 'mecha10 init' first.",
51 path.display()
52 );
53 }
54
55 let content = tokio::fs::read_to_string(path)
56 .await
57 .with_context(|| format!("Failed to read configuration file: {}", path.display()))?;
58
59 let config: ProjectConfig = serde_json::from_str(&content)
60 .with_context(|| format!("Failed to parse configuration file: {}", path.display()))?;
61
62 Ok(config)
63 }
64
65 /// Find mecha10.json in the current directory or any parent directory
66 ///
67 /// Searches upward from the current working directory until it finds
68 /// a mecha10.json file or reaches the root directory.
69 ///
70 /// # Returns
71 ///
72 /// Returns the path to the found configuration file.
73 ///
74 /// # Errors
75 ///
76 /// Returns an error if no mecha10.json file is found in the current
77 /// directory or any parent directory.
78 pub fn find_config() -> Result<PathBuf> {
79 Self::find_config_from(&std::env::current_dir()?)
80 }
81
82 /// Find mecha10.json starting from a specific directory
83 ///
84 /// # Arguments
85 ///
86 /// * `start_dir` - Directory to start searching from
87 ///
88 /// # Returns
89 ///
90 /// Returns the path to the found configuration file.
91 ///
92 /// # Errors
93 ///
94 /// Returns an error if no mecha10.json file is found.
95 pub fn find_config_from(start_dir: &Path) -> Result<PathBuf> {
96 let mut current_dir = start_dir.to_path_buf();
97
98 loop {
99 let config_path = current_dir.join(paths::PROJECT_CONFIG);
100 if config_path.exists() {
101 return Ok(config_path);
102 }
103
104 // Try parent directory
105 match current_dir.parent() {
106 Some(parent) => current_dir = parent.to_path_buf(),
107 None => {
108 anyhow::bail!(
109 "No mecha10.json found in {} or any parent directory.\n\n\
110 Run 'mecha10 init' to create a new project.",
111 start_dir.display()
112 )
113 }
114 }
115 }
116 }
117
118 /// Check if a project is initialized in the given directory
119 ///
120 /// # Arguments
121 ///
122 /// * `dir` - Directory to check
123 ///
124 /// # Returns
125 ///
126 /// Returns `true` if mecha10.json exists in the directory.
127 pub fn is_initialized(dir: &Path) -> bool {
128 dir.join(paths::PROJECT_CONFIG).exists()
129 }
130
131 /// Check if a project is initialized in the current directory
132 pub fn is_initialized_here() -> bool {
133 PathBuf::from(paths::PROJECT_CONFIG).exists()
134 }
135
136 /// Load robot ID from configuration file
137 ///
138 /// This is a convenience method that loads just the robot ID
139 /// without parsing the entire configuration.
140 ///
141 /// # Arguments
142 ///
143 /// * `path` - Path to the mecha10.json file
144 ///
145 /// # Errors
146 ///
147 /// Returns an error if the file cannot be read or parsed.
148 pub async fn load_robot_id(path: &Path) -> Result<String> {
149 let config = Self::load_from(path).await?;
150 Ok(config.robot.id)
151 }
152
153 /// Validate configuration file
154 ///
155 /// Uses the mecha10-core schema validation to check if the configuration
156 /// is valid according to the JSON schema and custom validation rules.
157 ///
158 /// # Arguments
159 ///
160 /// * `path` - Path to the configuration file
161 ///
162 /// # Errors
163 ///
164 /// Returns an error if validation fails with details about what's wrong.
165 pub fn validate(path: &Path) -> Result<()> {
166 use mecha10_core::schema_validation::validate_project_config;
167
168 if !path.exists() {
169 anyhow::bail!(
170 "Configuration file not found: {}\n\nRun 'mecha10 init' to create a new project.",
171 path.display()
172 );
173 }
174
175 validate_project_config(path).context("Configuration validation failed")
176 }
177
178 /// Resolve the config file to validate and validate it -- the single
179 /// implementation shared by `mecha10 config validate` and the config-validation
180 /// step in `mecha10 lint`, so neither re-implements this logic locally.
181 ///
182 /// * `path` is `None` (the default): finds the project's `mecha10.json` via
183 /// [`Self::find_config`] and validates it plus every node config it references
184 /// (via `mecha10_core::schema_validation::validate_project_config`).
185 /// * `path` is `Some`: validates exactly that one file, auto-detecting whether
186 /// it's a project config or a node config (via
187 /// `mecha10_core::schema_validation::validate_config_file`) -- no recursion
188 /// into other files. This is the mode a git hook uses to check only the files
189 /// staged in a commit.
190 ///
191 /// # Returns
192 ///
193 /// The path that was validated, so callers can report it.
194 ///
195 /// # Errors
196 ///
197 /// Returns an error with the underlying schema-validation failure details if the
198 /// target file doesn't exist or fails validation.
199 pub fn resolve_and_validate(path: Option<&Path>) -> Result<PathBuf> {
200 Self::resolve_and_validate_with_node_schema_resolver(path, None)
201 }
202
203 /// Same as [`Self::resolve_and_validate`], but lets a caller supply a
204 /// [`mecha10_core::schema_validation::NodeSchemaResolver`] so each referenced
205 /// node config validates against a node-type-specific schema instead of the
206 /// generic envelope (LAB-1772).
207 ///
208 /// This crate can't construct a resolver itself -- doing so needs each node's
209 /// concrete config struct, and `mecha10-cli-core` can't depend on
210 /// `packages/nodes/*` (see `docs/validate-configs.md`) -- so it only accepts one
211 /// as a parameter. `packages/cli` builds the actual resolver (via
212 /// `mecha10-schema-registry`, which *can* depend on the node crates) and passes
213 /// it in here.
214 ///
215 /// # Errors
216 ///
217 /// Returns an error with the underlying schema-validation failure details if the
218 /// target file doesn't exist or fails validation.
219 pub fn resolve_and_validate_with_node_schema_resolver(
220 path: Option<&Path>,
221 node_schema_resolver: Option<&mecha10_core::schema_validation::NodeSchemaResolver>,
222 ) -> Result<PathBuf> {
223 use mecha10_core::schema_validation::{
224 validate_config_file_with_node_schema_resolver, validate_project_config_with_node_schema_resolver,
225 };
226
227 let target = match path {
228 Some(p) => p.to_path_buf(),
229 None => Self::find_config()?,
230 };
231
232 if !target.exists() {
233 anyhow::bail!("Configuration file not found: {}", target.display());
234 }
235
236 let result = match path {
237 Some(_) => validate_config_file_with_node_schema_resolver(&target, node_schema_resolver),
238 None => validate_project_config_with_node_schema_resolver(&target, node_schema_resolver),
239 };
240
241 result.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", target.display(), e))?;
242
243 Ok(target)
244 }
245}