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 {
14 root: PathBuf,
15}
16
17impl ProjectService {
18 pub fn detect(path: &Path) -> Result<Self> {
30 let start = path.canonicalize().context("Failed to canonicalize path")?;
31
32 mecha10_core::fs_utils::find_project_root(&start)
33 .map(|root| Self { root })
34 .ok_or_else(|| {
35 anyhow!(
36 "No mecha10.json found in {} or any parent directory.\n\
37 Run 'mecha10 init' to create a new project.",
38 path.display()
39 )
40 })
41 }
42
43 pub fn new(path: PathBuf) -> Self {
53 Self { root: path }
54 }
55
56 pub fn root(&self) -> &Path {
58 &self.root
59 }
60
61 pub fn config_path(&self) -> PathBuf {
63 self.root.join(paths::PROJECT_CONFIG)
64 }
65
66 pub fn is_initialized(&self) -> bool {
68 self.config_path().exists()
69 }
70
71 pub fn name(&self) -> Result<String> {
75 let (name, _) = self.load_metadata()?;
76 Ok(name)
77 }
78
79 pub fn version(&self) -> Result<String> {
83 let (_, version) = self.load_metadata()?;
84 Ok(version)
85 }
86
87 pub fn load_metadata(&self) -> Result<(String, String)> {
91 let mecha10_json = self.config_path();
92 if mecha10_json.exists() {
93 let content = fs::read_to_string(&mecha10_json).context("Failed to read mecha10.json")?;
94 let json: serde_json::Value = serde_json::from_str(&content).context("Failed to parse mecha10.json")?;
95
96 let name = json["name"]
97 .as_str()
98 .ok_or_else(|| anyhow!("Missing 'name' field in mecha10.json"))?
99 .to_string();
100
101 let version = json["version"]
102 .as_str()
103 .ok_or_else(|| anyhow!("Missing 'version' field in mecha10.json"))?
104 .to_string();
105
106 return Ok((name, version));
107 }
108
109 let cargo_toml = self.root.join(paths::rust::CARGO_TOML);
110 if cargo_toml.exists() {
111 let content = fs::read_to_string(&cargo_toml).context("Failed to read Cargo.toml")?;
112
113 let toml: toml::Value = content.parse().context("Failed to parse Cargo.toml")?;
114
115 let name = toml
116 .get("package")
117 .and_then(|p| p.get("name"))
118 .and_then(|n| n.as_str())
119 .ok_or_else(|| anyhow!("Missing 'package.name' in Cargo.toml"))?
120 .to_string();
121
122 let version = toml
123 .get("package")
124 .and_then(|p| p.get("version"))
125 .and_then(|v| v.as_str())
126 .ok_or_else(|| anyhow!("Missing 'package.version' in Cargo.toml"))?
127 .to_string();
128
129 return Ok((name, version));
130 }
131
132 Err(anyhow!(
133 "No mecha10.json or Cargo.toml found in project root: {}",
134 self.root.display()
135 ))
136 }
137
138 pub fn validate(&self) -> Result<()> {
142 if !self.config_path().exists() {
143 return Err(anyhow!(
144 "Project not initialized: mecha10.json not found at {}",
145 self.root.display()
146 ));
147 }
148
149 let required_dirs = vec!["nodes", "drivers", "types"];
150 for dir in required_dirs {
151 let dir_path = self.root.join(dir);
152 if !dir_path.exists() {
153 return Err(anyhow!("Invalid project structure: missing '{}' directory", dir));
154 }
155 }
156
157 Ok(())
158 }
159
160 pub fn list_nodes(&self) -> Result<Vec<String>> {
164 let nodes_dir = self.root.join(paths::project::NODES_DIR);
165 self.list_directories(&nodes_dir)
166 }
167
168 pub fn list_drivers(&self) -> Result<Vec<String>> {
172 self.list_project_subdir(paths::project::DRIVERS_DIR)
173 }
174
175 pub fn list_types(&self) -> Result<Vec<String>> {
179 self.list_project_subdir(paths::project::TYPES_DIR)
180 }
181
182 fn list_project_subdir(&self, relative: &str) -> Result<Vec<String>> {
186 self.list_directories(&self.root.join(relative))
187 }
188
189 pub async fn list_enabled_nodes(&self) -> Result<Vec<String>> {
195 use crate::services::ConfigService;
196
197 let config = ConfigService::load_from(&self.config_path()).await?;
198 Ok(config.nodes.get_node_names())
199 }
200
201 fn list_directories(&self, dir: &Path) -> Result<Vec<String>> {
203 if !dir.exists() {
204 return Ok(Vec::new());
205 }
206
207 let mut names = Vec::new();
208 for entry in fs::read_dir(dir).with_context(|| format!("Failed to read directory: {}", dir.display()))? {
209 let entry = entry?;
210 if entry.file_type()?.is_dir() {
211 if let Some(name) = entry.file_name().to_str() {
212 names.push(name.to_string());
213 }
214 }
215 }
216
217 names.sort();
218 Ok(names)
219 }
220
221 pub fn path(&self, relative: &str) -> PathBuf {
223 self.root.join(relative)
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::ProjectService;
230
231 #[test]
236 fn list_drivers_and_list_types_list_directory_names_in_their_own_subdir() {
237 let root = tempfile::tempdir().expect("tempdir");
238 std::fs::create_dir_all(root.path().join("drivers/servo")).unwrap();
239 std::fs::create_dir_all(root.path().join("drivers/camera")).unwrap();
240 std::fs::create_dir_all(root.path().join("types/pose")).unwrap();
241
242 let service = ProjectService::new(root.path().to_path_buf());
243
244 assert_eq!(
245 service.list_drivers().unwrap(),
246 vec!["camera".to_string(), "servo".to_string()]
247 );
248 assert_eq!(service.list_types().unwrap(), vec!["pose".to_string()]);
249 }
250
251 #[test]
253 fn list_drivers_and_list_types_return_empty_when_subdir_is_absent() {
254 let root = tempfile::tempdir().expect("tempdir");
255 let service = ProjectService::new(root.path().to_path_buf());
256
257 assert!(service.list_drivers().unwrap().is_empty());
258 assert!(service.list_types().unwrap().is_empty());
259 }
260}