mecha10_cli_core/services/
project.rs1#![allow(dead_code)]
2
3use crate::paths;
8use anyhow::{anyhow, Context, Result};
9use std::fs;
10use std::path::{Path, PathBuf};
11
12pub struct ProjectService {
13 root: PathBuf,
14}
15
16impl ProjectService {
17 pub fn detect(path: &Path) -> Result<Self> {
19 let start = path.canonicalize().context("Failed to canonicalize path")?;
20
21 mecha10_core::fs_utils::find_project_root(&start)
22 .map(|root| Self { root })
23 .ok_or_else(|| {
24 anyhow!(
25 "No mecha10.json found in {} or any parent directory.\n\
26 Run 'mecha10 init' to create a new project.",
27 path.display()
28 )
29 })
30 }
31
32 pub fn new(path: PathBuf) -> Self {
35 Self { root: path }
36 }
37
38 pub fn root(&self) -> &Path {
40 &self.root
41 }
42
43 pub fn config_path(&self) -> PathBuf {
45 self.root.join(paths::PROJECT_CONFIG)
46 }
47
48 pub fn is_initialized(&self) -> bool {
50 self.config_path().exists()
51 }
52
53 pub fn name(&self) -> Result<String> {
57 let (name, _) = self.load_metadata()?;
58 Ok(name)
59 }
60
61 pub fn version(&self) -> Result<String> {
65 let (_, version) = self.load_metadata()?;
66 Ok(version)
67 }
68
69 pub fn load_metadata(&self) -> Result<(String, String)> {
73 let mecha10_json = self.config_path();
74 if mecha10_json.exists() {
75 let content = fs::read_to_string(&mecha10_json).context("Failed to read mecha10.json")?;
76 let json: serde_json::Value = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
77
78 let name = json["name"]
79 .as_str()
80 .ok_or_else(|| anyhow!("Missing 'name' field in mecha10.json"))?
81 .to_string();
82
83 let version = json["version"]
84 .as_str()
85 .ok_or_else(|| anyhow!("Missing 'version' field in mecha10.json"))?
86 .to_string();
87
88 return Ok((name, version));
89 }
90
91 let cargo_toml = self.root.join(paths::rust::CARGO_TOML);
92 if cargo_toml.exists() {
93 let content = fs::read_to_string(&cargo_toml).context("Failed to read Cargo.toml")?;
94
95 let toml: toml::Value = content.parse().context("Failed to parse Cargo.toml")?;
96
97 let name = toml
98 .get("package")
99 .and_then(|p| p.get("name"))
100 .and_then(|n| n.as_str())
101 .ok_or_else(|| anyhow!("Missing 'package.name' in Cargo.toml"))?
102 .to_string();
103
104 let version = toml
105 .get("package")
106 .and_then(|p| p.get("version"))
107 .and_then(|v| v.as_str())
108 .ok_or_else(|| anyhow!("Missing 'package.version' in Cargo.toml"))?
109 .to_string();
110
111 return Ok((name, version));
112 }
113
114 Err(anyhow!(
115 "No mecha10.json or Cargo.toml found in project root: {}",
116 self.root.display()
117 ))
118 }
119
120 pub fn validate(&self) -> Result<()> {
124 if !self.config_path().exists() {
125 return Err(anyhow!(
126 "Project not initialized: mecha10.json not found at {}",
127 self.root.display()
128 ));
129 }
130
131 let required_dirs = vec!["nodes", "drivers", "types"];
132 for dir in required_dirs {
133 let dir_path = self.root.join(dir);
134 if !dir_path.exists() {
135 return Err(anyhow!("Invalid project structure: missing '{}' directory", dir));
136 }
137 }
138
139 Ok(())
140 }
141
142 pub fn list_nodes(&self) -> Result<Vec<String>> {
146 let nodes_dir = self.root.join(paths::project::NODES_DIR);
147 self.list_directories(&nodes_dir)
148 }
149
150 pub fn list_drivers(&self) -> Result<Vec<String>> {
154 self.list_project_subdir(paths::project::DRIVERS_DIR)
155 }
156
157 pub fn list_types(&self) -> Result<Vec<String>> {
161 self.list_project_subdir(paths::project::TYPES_DIR)
162 }
163
164 fn list_project_subdir(&self, relative: &str) -> Result<Vec<String>> {
168 self.list_directories(&self.root.join(relative))
169 }
170
171 pub async fn list_enabled_nodes(&self) -> Result<Vec<String>> {
174 use crate::services::ConfigService;
175
176 let config = ConfigService::load_from(&self.config_path()).await?;
177 Ok(config.nodes.get_node_names())
178 }
179
180 fn list_directories(&self, dir: &Path) -> Result<Vec<String>> {
182 if !dir.exists() {
183 return Ok(Vec::new());
184 }
185
186 let mut names = Vec::new();
187 for entry in fs::read_dir(dir).with_context(|| format!("Failed to read directory: {}", dir.display()))? {
188 let entry = entry?;
189 if entry.file_type()?.is_dir() {
190 if let Some(name) = entry.file_name().to_str() {
191 names.push(name.to_string());
192 }
193 }
194 }
195
196 names.sort();
197 Ok(names)
198 }
199
200 pub fn path(&self, relative: &str) -> PathBuf {
202 self.root.join(relative)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::ProjectService;
209
210 #[test]
213 fn list_drivers_and_list_types_list_directory_names_in_their_own_subdir() {
214 let root = tempfile::tempdir().expect("tempdir");
215 std::fs::create_dir_all(root.path().join("drivers/servo")).unwrap();
216 std::fs::create_dir_all(root.path().join("drivers/camera")).unwrap();
217 std::fs::create_dir_all(root.path().join("types/pose")).unwrap();
218
219 let service = ProjectService::new(root.path().to_path_buf());
220
221 assert_eq!(
222 service.list_drivers().unwrap(),
223 vec!["camera".to_string(), "servo".to_string()]
224 );
225 assert_eq!(service.list_types().unwrap(), vec!["pose".to_string()]);
226 }
227
228 #[test]
230 fn list_drivers_and_list_types_return_empty_when_subdir_is_absent() {
231 let root = tempfile::tempdir().expect("tempdir");
232 let service = ProjectService::new(root.path().to_path_buf());
233
234 assert!(service.list_drivers().unwrap().is_empty());
235 assert!(service.list_types().unwrap().is_empty());
236 }
237}