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 match current_dir.parent() {
105 Some(parent) => current_dir = parent.to_path_buf(),
106 None => {
107 anyhow::bail!(
108 "No mecha10.json found in {} or any parent directory.\n\n\
109 Run 'mecha10 init' to create a new project.",
110 start_dir.display()
111 )
112 }
113 }
114 }
115 }
116
117 /// Check if a project is initialized in the given directory
118 ///
119 /// # Arguments
120 ///
121 /// * `dir` - Directory to check
122 ///
123 /// # Returns
124 ///
125 /// Returns `true` if mecha10.json exists in the directory.
126 pub fn is_initialized(dir: &Path) -> bool {
127 dir.join(paths::PROJECT_CONFIG).exists()
128 }
129
130 /// Check if a project is initialized in the current directory
131 pub fn is_initialized_here() -> bool {
132 PathBuf::from(paths::PROJECT_CONFIG).exists()
133 }
134
135 /// Load robot ID from configuration file
136 ///
137 /// This is a convenience method that loads just the robot ID
138 /// without parsing the entire configuration.
139 ///
140 /// # Arguments
141 ///
142 /// * `path` - Path to the mecha10.json file
143 ///
144 /// # Errors
145 ///
146 /// Returns an error if the file cannot be read or parsed.
147 pub async fn load_robot_id(path: &Path) -> Result<String> {
148 let config = Self::load_from(path).await?;
149 Ok(config.robot.id)
150 }
151
152 /// Validate configuration file
153 ///
154 /// Uses the mecha10-core schema validation to check if the configuration
155 /// is valid according to the JSON schema and custom validation rules.
156 ///
157 /// # Arguments
158 ///
159 /// * `path` - Path to the configuration file
160 ///
161 /// # Errors
162 ///
163 /// Returns an error if validation fails with details about what's wrong.
164 pub fn validate(path: &Path) -> Result<()> {
165 use mecha10_core::schema_validation::validate_project_config;
166
167 if !path.exists() {
168 anyhow::bail!(
169 "Configuration file not found: {}\n\nRun 'mecha10 init' to create a new project.",
170 path.display()
171 );
172 }
173
174 validate_project_config(path).context("Configuration validation failed")
175 }
176
177 /// Resolve the config file to validate and validate it -- the single
178 /// implementation shared by `mecha10 config validate` and the config-validation
179 /// step in `mecha10 lint`, so neither re-implements this logic locally.
180 ///
181 /// * `path` is `None` (the default): finds the project's `mecha10.json` via
182 /// [`Self::find_config`] and validates it plus every node config it references
183 /// (via `mecha10_core::schema_validation::validate_project_config`).
184 /// * `path` is `Some`: validates exactly that one file, auto-detecting whether
185 /// it's a project config or a node config (via
186 /// `mecha10_core::schema_validation::validate_config_file`) -- no recursion
187 /// into other files. This is the mode a git hook uses to check only the files
188 /// staged in a commit.
189 ///
190 /// # Returns
191 ///
192 /// The path that was validated, so callers can report it.
193 ///
194 /// # Errors
195 ///
196 /// Returns an error with the underlying schema-validation failure details if the
197 /// target file doesn't exist or fails validation.
198 pub fn resolve_and_validate(path: Option<&Path>) -> Result<PathBuf> {
199 Self::resolve_and_validate_with_node_schema_resolver(path, None)
200 }
201
202 /// Same as [`Self::resolve_and_validate`], but lets a caller supply a
203 /// [`mecha10_core::schema_validation::NodeSchemaResolver`] so each referenced
204 /// node config validates against a node-type-specific schema instead of the
205 /// generic envelope (LAB-1772).
206 ///
207 /// This crate can't construct a resolver itself -- doing so needs each node's
208 /// concrete config struct, and `mecha10-cli-core` can't depend on
209 /// `packages/nodes/*` (see `docs/validate-configs.md`) -- so it only accepts one
210 /// as a parameter. `packages/cli` builds the actual resolver (via
211 /// `mecha10-schema-registry`, which *can* depend on the node crates) and passes
212 /// it in here.
213 ///
214 /// # Errors
215 ///
216 /// Returns an error with the underlying schema-validation failure details if the
217 /// target file doesn't exist or fails validation.
218 pub fn resolve_and_validate_with_node_schema_resolver(
219 path: Option<&Path>,
220 node_schema_resolver: Option<&mecha10_core::schema_validation::NodeSchemaResolver>,
221 ) -> Result<PathBuf> {
222 use mecha10_core::schema_validation::{
223 validate_config_file_with_node_schema_resolver, validate_project_config_with_node_schema_resolver,
224 };
225
226 let target = match path {
227 Some(p) => p.to_path_buf(),
228 None => Self::find_config()?,
229 };
230
231 if !target.exists() {
232 anyhow::bail!("Configuration file not found: {}", target.display());
233 }
234
235 let result = match path {
236 Some(_) => validate_config_file_with_node_schema_resolver(&target, node_schema_resolver),
237 None => validate_project_config_with_node_schema_resolver(&target, node_schema_resolver),
238 };
239
240 result.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", target.display(), e))?;
241
242 Ok(target)
243 }
244}