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
13pub struct ConfigService;
14
15impl ConfigService {
16 /// Load project configuration from a specific path
17 ///
18 /// # Arguments
19 ///
20 /// * `path` - Path to the mecha10.json file
21 ///
22 /// # Errors
23 ///
24 /// Returns an error if:
25 /// - The file doesn't exist
26 /// - The file cannot be read
27 /// - The JSON is invalid
28 /// - The configuration doesn't match the expected schema
29 pub async fn load_from(path: &Path) -> Result<ProjectConfig> {
30 if !path.exists() {
31 anyhow::bail!(
32 "Project configuration not found at {}. Run 'mecha10 init' first.",
33 path.display()
34 );
35 }
36
37 let content = tokio::fs::read_to_string(path)
38 .await
39 .with_context(|| format!("Failed to read configuration file: {}", path.display()))?;
40
41 let config: ProjectConfig = serde_json::from_str(&content)
42 .with_context(|| format!("Failed to parse configuration file: {}", path.display()))?;
43
44 Ok(config)
45 }
46
47 /// Find mecha10.json in the current directory or any parent directory
48 ///
49 /// Searches upward from the current working directory until it finds
50 /// a mecha10.json file or reaches the root directory.
51 ///
52 /// # Returns
53 ///
54 /// Returns the path to the found configuration file.
55 ///
56 /// # Errors
57 ///
58 /// Returns an error if no mecha10.json file is found in the current
59 /// directory or any parent directory.
60 pub fn find_config() -> Result<PathBuf> {
61 Self::find_config_from(&std::env::current_dir()?)
62 }
63
64 /// Find mecha10.json starting from a specific directory
65 ///
66 /// # Arguments
67 ///
68 /// * `start_dir` - Directory to start searching from
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.
77 pub fn find_config_from(start_dir: &Path) -> Result<PathBuf> {
78 let mut current_dir = start_dir.to_path_buf();
79
80 loop {
81 let config_path = current_dir.join(paths::PROJECT_CONFIG);
82 if config_path.exists() {
83 return Ok(config_path);
84 }
85
86 match current_dir.parent() {
87 Some(parent) => current_dir = parent.to_path_buf(),
88 None => {
89 anyhow::bail!(
90 "No mecha10.json found in {} or any parent directory.\n\n\
91 Run 'mecha10 init' to create a new project.",
92 start_dir.display()
93 )
94 }
95 }
96 }
97 }
98
99 /// Check if a project is initialized in the given directory
100 ///
101 /// # Arguments
102 ///
103 /// * `dir` - Directory to check
104 ///
105 /// # Returns
106 ///
107 /// Returns `true` if mecha10.json exists in the directory.
108 pub fn is_initialized(dir: &Path) -> bool {
109 dir.join(paths::PROJECT_CONFIG).exists()
110 }
111
112 /// Check if a project is initialized in the current directory
113 pub fn is_initialized_here() -> bool {
114 PathBuf::from(paths::PROJECT_CONFIG).exists()
115 }
116
117 /// Load robot ID from configuration file
118 ///
119 /// This is a convenience method that loads just the robot ID
120 /// without parsing the entire configuration.
121 ///
122 /// # Arguments
123 ///
124 /// * `path` - Path to the mecha10.json file
125 ///
126 /// # Errors
127 ///
128 /// Returns an error if the file cannot be read or parsed.
129 pub async fn load_robot_id(path: &Path) -> Result<String> {
130 let config = Self::load_from(path).await?;
131 Ok(config.robot.id)
132 }
133
134 /// Validate configuration file
135 ///
136 /// Uses the mecha10-core schema validation to check if the configuration
137 /// is valid according to the JSON schema and custom validation rules.
138 ///
139 /// # Arguments
140 ///
141 /// * `path` - Path to the configuration file
142 ///
143 /// # Errors
144 ///
145 /// Returns an error if validation fails with details about what's wrong.
146 pub fn validate(path: &Path) -> Result<()> {
147 use mecha10_core::schema_validation::validate_project_config;
148
149 if !path.exists() {
150 anyhow::bail!(
151 "Configuration file not found: {}\n\nRun 'mecha10 init' to create a new project.",
152 path.display()
153 );
154 }
155
156 validate_project_config(path).context("Configuration validation failed")
157 }
158
159 /// Resolve the config file to validate and validate it -- the single
160 /// implementation shared by `mecha10 config validate` and the config-validation
161 /// step in `mecha10 lint`, so neither re-implements this logic locally.
162 ///
163 /// * `path` is `None` (the default): finds the project's `mecha10.json` via
164 /// [`Self::find_config`] and validates it plus every node config it references
165 /// (via `mecha10_core::schema_validation::validate_project_config`).
166 /// * `path` is `Some`: validates exactly that one file, auto-detecting whether
167 /// it's a project config or a node config (via
168 /// `mecha10_core::schema_validation::validate_config_file`) -- no recursion
169 /// into other files. This is the mode a git hook uses to check only the files
170 /// staged in a commit.
171 ///
172 /// Every referenced node config, built-in or custom, resolves its declared schema via a
173 /// `<name>.schema.json` file shipped alongside it (LAB-4388/LAB-4390) -- a node with no
174 /// declared schema hard-errors naming it (LAB-4066; there is no generic envelope to fall
175 /// back to).
176 ///
177 /// # Returns
178 ///
179 /// The path that was validated, so callers can report it.
180 ///
181 /// # Errors
182 ///
183 /// Returns an error with the underlying schema-validation failure details if the
184 /// target file doesn't exist or fails validation.
185 pub fn resolve_and_validate(path: Option<&Path>) -> Result<PathBuf> {
186 use mecha10_core::schema_validation::{validate_config_file, validate_project_config};
187
188 let target = match path {
189 Some(p) => p.to_path_buf(),
190 None => Self::find_config()?,
191 };
192
193 if !target.exists() {
194 anyhow::bail!("Configuration file not found: {}", target.display());
195 }
196
197 let result = match path {
198 Some(_) => validate_config_file(&target),
199 None => validate_project_config(&target),
200 };
201
202 result.map_err(|e| anyhow::anyhow!("Configuration validation failed for {}: {}", target.display(), e))?;
203
204 Ok(target)
205 }
206}