Skip to main content

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}