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