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