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