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
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}