mecha10_templates/services/
driver_generator.rs1use crate::paths;
12use crate::services::cargo_workspace::add_workspace_member;
13use crate::templates::render::render;
14use crate::utils::to_pascal_case;
15use anyhow::{Context, Result};
16use std::path::Path;
17
18const CARGO_TOML_TEMPLATE: &str = include_str!("../../templates/drivers/Cargo.toml.template");
20const MAIN_RS_TEMPLATE: &str = include_str!("../../templates/drivers/main.rs.template");
22const CAMERA_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/camera.rs.template");
24const MOTOR_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/motor.rs.template");
26const SENSOR_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/sensor.rs.template");
28const IMU_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/imu.rs.template");
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DriverType {
38 Camera,
39 Motor,
40 Sensor,
41 Imu,
42}
43
44impl DriverType {
45 pub const ALL: [DriverType; 4] = [
48 DriverType::Camera,
49 DriverType::Motor,
50 DriverType::Sensor,
51 DriverType::Imu,
52 ];
53
54 pub fn as_str(self) -> &'static str {
61 match self {
62 DriverType::Camera => "camera",
63 DriverType::Motor => "motor",
64 DriverType::Sensor => "sensor",
65 DriverType::Imu => "imu",
66 }
67 }
68
69 pub fn parse(value: &str) -> Result<Self> {
73 DriverType::ALL
74 .into_iter()
75 .find(|kind| kind.as_str().eq_ignore_ascii_case(value))
76 .ok_or_else(|| {
77 anyhow::anyhow!(
78 "Unknown driver type '{}'. Available driver types: {}",
79 value,
80 DriverType::ALL.map(|kind| kind.as_str()).join(", ")
81 )
82 })
83 }
84
85 fn lib_rs_template(self) -> &'static str {
87 match self {
88 DriverType::Camera => CAMERA_LIB_RS_TEMPLATE,
89 DriverType::Motor => MOTOR_LIB_RS_TEMPLATE,
90 DriverType::Sensor => SENSOR_LIB_RS_TEMPLATE,
91 DriverType::Imu => IMU_LIB_RS_TEMPLATE,
92 }
93 }
94}
95
96pub struct DriverGeneratorService;
98
99impl DriverGeneratorService {
100 pub fn new() -> Self {
102 Self
103 }
104
105 pub async fn generate_driver(&self, project_root: &Path, name: &str, driver_type: DriverType) -> Result<()> {
116 self.validate_driver_name(name)?;
117
118 let driver_dir = project_root.join(paths::project::DRIVERS_DIR).join(name);
119 if driver_dir.exists() {
120 anyhow::bail!("Driver '{}' already exists at {}", name, driver_dir.display());
121 }
122
123 let src_dir = driver_dir.join(paths::project::SRC_DIR);
124 tokio::fs::create_dir_all(&src_dir)
125 .await
126 .context("Failed to create driver directory")?;
127
128 let crate_name = self.to_snake_case(name);
129 let pascal_name = to_pascal_case(&crate_name);
130
131 let cargo_toml = render(CARGO_TOML_TEMPLATE, &[("name", name), ("crate_name", &crate_name)]);
132 tokio::fs::write(driver_dir.join(paths::rust::CARGO_TOML), cargo_toml).await?;
133
134 let main_rs = render(MAIN_RS_TEMPLATE, &[("name", name), ("crate_name", &crate_name)]);
135 tokio::fs::write(src_dir.join("main.rs"), main_rs).await?;
136
137 let lib_rs = render(
138 driver_type.lib_rs_template(),
139 &[("name", name), ("PascalName", &pascal_name)],
140 );
141 tokio::fs::write(src_dir.join("lib.rs"), lib_rs).await?;
142
143 add_workspace_member(project_root, &format!("{}/{}", paths::project::DRIVERS_DIR, name)).await?;
144
145 Ok(())
146 }
147
148 fn validate_driver_name(&self, name: &str) -> Result<()> {
154 crate::services::generator_name::validate_generator_name("Driver", name)
155 }
156
157 fn to_snake_case(&self, name: &str) -> String {
160 name.replace('-', "_")
161 }
162}
163
164impl Default for DriverGeneratorService {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170#[cfg(test)]
171mod tests {
172 use super::*;
173 use tempfile::TempDir;
174
175 async fn setup_project() -> TempDir {
176 let temp = TempDir::new().unwrap();
177 tokio::fs::write(temp.path().join("Cargo.toml"), "[workspace]\nmembers = []\n")
178 .await
179 .unwrap();
180 temp
181 }
182
183 #[test]
184 fn driver_type_parse_accepts_all_known_types() {
185 assert_eq!(DriverType::parse("camera").unwrap(), DriverType::Camera);
186 assert_eq!(DriverType::parse("MOTOR").unwrap(), DriverType::Motor);
187 assert_eq!(DriverType::parse("Sensor").unwrap(), DriverType::Sensor);
188 assert_eq!(DriverType::parse("imu").unwrap(), DriverType::Imu);
189 }
190
191 #[test]
192 fn driver_type_parse_rejects_unknown_type_and_lists_available_ones() {
193 let err = DriverType::parse("gripper").unwrap_err();
194 assert!(err
195 .to_string()
196 .contains("Available driver types: camera, motor, sensor, imu"));
197 }
198
199 #[test]
200 fn validate_driver_name_rejects_leading_digit() {
201 let service = DriverGeneratorService::new();
202 let err = service.validate_driver_name("1camera").unwrap_err();
203 assert!(err.to_string().contains("must start with a letter"));
204 }
205
206 #[tokio::test]
207 async fn generate_driver_creates_expected_file_structure() {
208 let temp = setup_project().await;
209 let service = DriverGeneratorService::new();
210
211 service
212 .generate_driver(temp.path(), "my-camera", DriverType::Camera)
213 .await
214 .unwrap();
215
216 let driver_dir = temp.path().join("drivers/my-camera");
217 assert!(driver_dir.join("Cargo.toml").exists());
218 assert!(driver_dir.join("src/lib.rs").exists());
219 assert!(driver_dir.join("src/main.rs").exists());
220 }
221
222 #[tokio::test]
223 async fn generate_driver_fails_if_driver_already_exists() {
224 let temp = setup_project().await;
225 let service = DriverGeneratorService::new();
226
227 service
228 .generate_driver(temp.path(), "motor1", DriverType::Motor)
229 .await
230 .unwrap();
231 let err = service
232 .generate_driver(temp.path(), "motor1", DriverType::Motor)
233 .await
234 .unwrap_err();
235
236 assert!(err.to_string().contains("already exists"));
237 }
238
239 #[tokio::test]
240 async fn generate_driver_lib_rs_uses_pascal_case_and_type_specific_content() {
241 let temp = setup_project().await;
242 let service = DriverGeneratorService::new();
243
244 service
245 .generate_driver(temp.path(), "front-imu", DriverType::Imu)
246 .await
247 .unwrap();
248
249 let lib_rs = tokio::fs::read_to_string(temp.path().join("drivers/front-imu/src/lib.rs"))
250 .await
251 .unwrap();
252 assert!(lib_rs.contains("pub struct FrontImuNode"));
253 assert!(lib_rs.contains("FrontImuConfig"));
254 assert!(lib_rs.contains("Topic::new(\"/front-imu/data\")"));
255 assert!(lib_rs.contains("mecha10_core::node_main!(FrontImuNode, \"front-imu\");"));
256 assert!(lib_rs.contains("pub struct ImuData"));
257 }
258
259 #[tokio::test]
260 async fn generate_driver_updates_cargo_workspace_members() {
261 let temp = setup_project().await;
262 let service = DriverGeneratorService::new();
263
264 service
265 .generate_driver(temp.path(), "wheel-motor", DriverType::Motor)
266 .await
267 .unwrap();
268
269 let content = tokio::fs::read_to_string(temp.path().join("Cargo.toml")).await.unwrap();
270 let doc: toml::Value = toml::from_str(&content).unwrap();
271 let members = doc["workspace"]["members"].as_array().unwrap();
272 assert!(members.iter().any(|m| m.as_str() == Some("drivers/wheel-motor")));
273 }
274
275 #[tokio::test]
276 async fn generate_driver_does_not_touch_mecha10_json() {
277 let temp = setup_project().await;
278 let service = DriverGeneratorService::new();
279
280 service
281 .generate_driver(temp.path(), "bench-sensor", DriverType::Sensor)
282 .await
283 .unwrap();
284
285 assert!(!temp.path().join("mecha10.json").exists());
286 }
287}