mecha10_cli_core/services/
project.rs1#![allow(dead_code)]
2
3use crate::paths;
9use anyhow::{anyhow, Context, Result};
10use std::fs;
11use std::path::{Path, PathBuf};
12
13pub struct ProjectService {
35 root: PathBuf,
37}
38
39impl ProjectService {
40 pub fn detect(path: &Path) -> Result<Self> {
52 let start = path.canonicalize().context("Failed to canonicalize path")?;
53
54 mecha10_core::fs_utils::find_project_root(&start)
55 .map(|root| Self { root })
56 .ok_or_else(|| {
57 anyhow!(
58 "No mecha10.json found in {} or any parent directory.\n\
59 Run 'mecha10 init' to create a new project.",
60 path.display()
61 )
62 })
63 }
64
65 pub fn new(path: PathBuf) -> Self {
75 Self { root: path }
76 }
77
78 pub fn root(&self) -> &Path {
80 &self.root
81 }
82
83 pub fn config_path(&self) -> PathBuf {
85 self.root.join(paths::PROJECT_CONFIG)
86 }
87
88 pub fn is_initialized(&self) -> bool {
90 self.config_path().exists()
91 }
92
93 pub fn name(&self) -> Result<String> {
97 let (name, _) = self.load_metadata()?;
98 Ok(name)
99 }
100
101 pub fn version(&self) -> Result<String> {
105 let (_, version) = self.load_metadata()?;
106 Ok(version)
107 }
108
109 pub fn load_metadata(&self) -> Result<(String, String)> {
113 let mecha10_json = self.config_path();
114 if mecha10_json.exists() {
115 let content = fs::read_to_string(&mecha10_json).context("Failed to read mecha10.json")?;
116 let json: serde_json::Value = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
117
118 let name = json["name"]
119 .as_str()
120 .ok_or_else(|| anyhow!("Missing 'name' field in mecha10.json"))?
121 .to_string();
122
123 let version = json["version"]
124 .as_str()
125 .ok_or_else(|| anyhow!("Missing 'version' field in mecha10.json"))?
126 .to_string();
127
128 return Ok((name, version));
129 }
130
131 let cargo_toml = self.root.join(paths::rust::CARGO_TOML);
132 if cargo_toml.exists() {
133 let content = fs::read_to_string(&cargo_toml).context("Failed to read Cargo.toml")?;
134
135 let toml: toml::Value = content.parse().context("Failed to parse Cargo.toml")?;
136
137 let name = toml
138 .get("package")
139 .and_then(|p| p.get("name"))
140 .and_then(|n| n.as_str())
141 .ok_or_else(|| anyhow!("Missing 'package.name' in Cargo.toml"))?
142 .to_string();
143
144 let version = toml
145 .get("package")
146 .and_then(|p| p.get("version"))
147 .and_then(|v| v.as_str())
148 .ok_or_else(|| anyhow!("Missing 'package.version' in Cargo.toml"))?
149 .to_string();
150
151 return Ok((name, version));
152 }
153
154 Err(anyhow!(
155 "No mecha10.json or Cargo.toml found in project root: {}",
156 self.root.display()
157 ))
158 }
159
160 pub fn validate(&self) -> Result<()> {
164 if !self.config_path().exists() {
165 return Err(anyhow!(
166 "Project not initialized: mecha10.json not found at {}",
167 self.root.display()
168 ));
169 }
170
171 let required_dirs = vec!["nodes", "drivers", "types"];
172 for dir in required_dirs {
173 let dir_path = self.root.join(dir);
174 if !dir_path.exists() {
175 return Err(anyhow!("Invalid project structure: missing '{}' directory", dir));
176 }
177 }
178
179 Ok(())
180 }
181
182 pub fn list_nodes(&self) -> Result<Vec<String>> {
186 let nodes_dir = self.root.join(paths::project::NODES_DIR);
187 self.list_directories(&nodes_dir)
188 }
189
190 pub fn list_drivers(&self) -> Result<Vec<String>> {
194 self.list_project_subdir(paths::project::DRIVERS_DIR)
195 }
196
197 pub fn list_types(&self) -> Result<Vec<String>> {
201 self.list_project_subdir(paths::project::TYPES_DIR)
202 }
203
204 fn list_project_subdir(&self, relative: &str) -> Result<Vec<String>> {
208 self.list_directories(&self.root.join(relative))
209 }
210
211 pub async fn list_enabled_nodes(&self) -> Result<Vec<String>> {
217 use crate::services::ConfigService;
218
219 let config = ConfigService::load_from(&self.config_path()).await?;
220 Ok(config.nodes.get_node_names())
221 }
222
223 fn list_directories(&self, dir: &Path) -> Result<Vec<String>> {
225 if !dir.exists() {
226 return Ok(Vec::new());
227 }
228
229 let mut names = Vec::new();
230 for entry in fs::read_dir(dir).with_context(|| format!("Failed to read directory: {}", dir.display()))? {
231 let entry = entry?;
232 if entry.file_type()?.is_dir() {
233 if let Some(name) = entry.file_name().to_str() {
234 names.push(name.to_string());
235 }
236 }
237 }
238
239 names.sort();
240 Ok(names)
241 }
242
243 pub fn path(&self, relative: &str) -> PathBuf {
245 self.root.join(relative)
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::ProjectService;
252
253 #[test]
258 fn list_drivers_and_list_types_list_directory_names_in_their_own_subdir() {
259 let root = tempfile::tempdir().expect("tempdir");
260 std::fs::create_dir_all(root.path().join("drivers/servo")).unwrap();
261 std::fs::create_dir_all(root.path().join("drivers/camera")).unwrap();
262 std::fs::create_dir_all(root.path().join("types/pose")).unwrap();
263
264 let service = ProjectService::new(root.path().to_path_buf());
265
266 assert_eq!(
267 service.list_drivers().unwrap(),
268 vec!["camera".to_string(), "servo".to_string()]
269 );
270 assert_eq!(service.list_types().unwrap(), vec!["pose".to_string()]);
271 }
272
273 #[test]
275 fn list_drivers_and_list_types_return_empty_when_subdir_is_absent() {
276 let root = tempfile::tempdir().expect("tempdir");
277 let service = ProjectService::new(root.path().to_path_buf());
278
279 assert!(service.list_drivers().unwrap().is_empty());
280 assert!(service.list_types().unwrap().is_empty());
281 }
282}