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");
19const MAIN_RS_TEMPLATE: &str = include_str!("../../templates/drivers/main.rs.template");
20const CAMERA_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/camera.rs.template");
21const MOTOR_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/motor.rs.template");
22const SENSOR_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/sensor.rs.template");
23const IMU_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/imu.rs.template");
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum DriverType {
27 Camera,
28 Motor,
29 Sensor,
30 Imu,
31}
32
33impl DriverType {
34 pub const ALL: [DriverType; 4] = [
35 DriverType::Camera,
36 DriverType::Motor,
37 DriverType::Sensor,
38 DriverType::Imu,
39 ];
40
41 pub fn as_str(self) -> &'static str {
43 match self {
44 DriverType::Camera => "camera",
45 DriverType::Motor => "motor",
46 DriverType::Sensor => "sensor",
47 DriverType::Imu => "imu",
48 }
49 }
50
51 pub fn parse(value: &str) -> Result<Self> {
55 DriverType::ALL
56 .into_iter()
57 .find(|kind| kind.as_str().eq_ignore_ascii_case(value))
58 .ok_or_else(|| {
59 anyhow::anyhow!(
60 "Unknown driver type '{}'. Available driver types: {}",
61 value,
62 DriverType::ALL.map(|kind| kind.as_str()).join(", ")
63 )
64 })
65 }
66
67 fn lib_rs_template(self) -> &'static str {
69 match self {
70 DriverType::Camera => CAMERA_LIB_RS_TEMPLATE,
71 DriverType::Motor => MOTOR_LIB_RS_TEMPLATE,
72 DriverType::Sensor => SENSOR_LIB_RS_TEMPLATE,
73 DriverType::Imu => IMU_LIB_RS_TEMPLATE,
74 }
75 }
76}
77
78pub struct DriverGeneratorService;
79
80impl DriverGeneratorService {
81 pub fn new() -> Self {
83 Self
84 }
85
86 pub async fn generate_driver(&self, project_root: &Path, name: &str, driver_type: DriverType) -> Result<()> {
97 self.validate_driver_name(name)?;
98
99 let driver_dir = project_root.join(paths::project::DRIVERS_DIR).join(name);
100 if driver_dir.exists() {
101 anyhow::bail!("Driver '{}' already exists at {}", name, driver_dir.display());
102 }
103
104 let src_dir = driver_dir.join(paths::project::SRC_DIR);
105 tokio::fs::create_dir_all(&src_dir)
106 .await
107 .context("Failed to create driver directory")?;
108
109 let crate_name = self.to_snake_case(name);
110 let pascal_name = to_pascal_case(&crate_name);
111
112 let cargo_toml = render(CARGO_TOML_TEMPLATE, &[("name", name), ("crate_name", &crate_name)]);
113 tokio::fs::write(driver_dir.join(paths::rust::CARGO_TOML), cargo_toml).await?;
114
115 let main_rs = render(MAIN_RS_TEMPLATE, &[("name", name), ("crate_name", &crate_name)]);
116 tokio::fs::write(src_dir.join("main.rs"), main_rs).await?;
117
118 let lib_rs = render(
119 driver_type.lib_rs_template(),
120 &[("name", name), ("PascalName", &pascal_name)],
121 );
122 tokio::fs::write(src_dir.join("lib.rs"), lib_rs).await?;
123
124 add_workspace_member(project_root, &format!("{}/{}", paths::project::DRIVERS_DIR, name)).await?;
125
126 Ok(())
127 }
128
129 fn validate_driver_name(&self, name: &str) -> Result<()> {
135 crate::services::generator_name::validate_generator_name("Driver", name)
136 }
137
138 fn to_snake_case(&self, name: &str) -> String {
141 name.replace('-', "_")
142 }
143}
144
145impl Default for DriverGeneratorService {
146 fn default() -> Self {
147 Self::new()
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use tempfile::TempDir;
155
156 async fn setup_project() -> TempDir {
157 let temp = TempDir::new().unwrap();
158 tokio::fs::write(temp.path().join("Cargo.toml"), "[workspace]\nmembers = []\n")
159 .await
160 .unwrap();
161 temp
162 }
163
164 #[test]
165 fn driver_type_parse_accepts_all_known_types() {
166 assert_eq!(DriverType::parse("camera").unwrap(), DriverType::Camera);
167 assert_eq!(DriverType::parse("MOTOR").unwrap(), DriverType::Motor);
168 assert_eq!(DriverType::parse("Sensor").unwrap(), DriverType::Sensor);
169 assert_eq!(DriverType::parse("imu").unwrap(), DriverType::Imu);
170 }
171
172 #[test]
173 fn driver_type_parse_rejects_unknown_type_and_lists_available_ones() {
174 let err = DriverType::parse("gripper").unwrap_err();
175 assert!(err
176 .to_string()
177 .contains("Available driver types: camera, motor, sensor, imu"));
178 }
179
180 #[test]
181 fn validate_driver_name_rejects_leading_digit() {
182 let service = DriverGeneratorService::new();
183 let err = service.validate_driver_name("1camera").unwrap_err();
184 assert!(err.to_string().contains("must start with a letter"));
185 }
186
187 #[tokio::test]
188 async fn generate_driver_creates_expected_file_structure() {
189 let temp = setup_project().await;
190 let service = DriverGeneratorService::new();
191
192 service
193 .generate_driver(temp.path(), "my-camera", DriverType::Camera)
194 .await
195 .unwrap();
196
197 let driver_dir = temp.path().join("drivers/my-camera");
198 assert!(driver_dir.join("Cargo.toml").exists());
199 assert!(driver_dir.join("src/lib.rs").exists());
200 assert!(driver_dir.join("src/main.rs").exists());
201 }
202
203 #[tokio::test]
204 async fn generate_driver_fails_if_driver_already_exists() {
205 let temp = setup_project().await;
206 let service = DriverGeneratorService::new();
207
208 service
209 .generate_driver(temp.path(), "motor1", DriverType::Motor)
210 .await
211 .unwrap();
212 let err = service
213 .generate_driver(temp.path(), "motor1", DriverType::Motor)
214 .await
215 .unwrap_err();
216
217 assert!(err.to_string().contains("already exists"));
218 }
219
220 #[tokio::test]
221 async fn generate_driver_lib_rs_uses_pascal_case_and_type_specific_content() {
222 let temp = setup_project().await;
223 let service = DriverGeneratorService::new();
224
225 service
226 .generate_driver(temp.path(), "front-imu", DriverType::Imu)
227 .await
228 .unwrap();
229
230 let lib_rs = tokio::fs::read_to_string(temp.path().join("drivers/front-imu/src/lib.rs"))
231 .await
232 .unwrap();
233 assert!(lib_rs.contains("pub struct FrontImuNode"));
234 assert!(lib_rs.contains("FrontImuConfig"));
235 assert!(lib_rs.contains("Topic::new(\"/front-imu/data\")"));
236 assert!(lib_rs.contains("mecha10_core::node_main!(FrontImuNode, \"front-imu\");"));
237 assert!(lib_rs.contains("pub struct ImuData"));
238 }
239
240 #[tokio::test]
241 async fn generate_driver_updates_cargo_workspace_members() {
242 let temp = setup_project().await;
243 let service = DriverGeneratorService::new();
244
245 service
246 .generate_driver(temp.path(), "wheel-motor", DriverType::Motor)
247 .await
248 .unwrap();
249
250 let content = tokio::fs::read_to_string(temp.path().join("Cargo.toml")).await.unwrap();
251 let doc: toml::Value = toml::from_str(&content).unwrap();
252 let members = doc["workspace"]["members"].as_array().unwrap();
253 assert!(members.iter().any(|m| m.as_str() == Some("drivers/wheel-motor")));
254 }
255
256 #[tokio::test]
257 async fn generate_driver_does_not_touch_mecha10_json() {
258 let temp = setup_project().await;
259 let service = DriverGeneratorService::new();
260
261 service
262 .generate_driver(temp.path(), "bench-sensor", DriverType::Sensor)
263 .await
264 .unwrap();
265
266 assert!(!temp.path().join("mecha10.json").exists());
267 }
268}