use crate::paths;
use crate::services::cargo_workspace::add_workspace_member;
use crate::templates::render::render;
use crate::utils::to_pascal_case;
use anyhow::{Context, Result};
use std::path::Path;
const CARGO_TOML_TEMPLATE: &str = include_str!("../../templates/drivers/Cargo.toml.template");
const MAIN_RS_TEMPLATE: &str = include_str!("../../templates/drivers/main.rs.template");
const CAMERA_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/camera.rs.template");
const MOTOR_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/motor.rs.template");
const SENSOR_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/sensor.rs.template");
const IMU_LIB_RS_TEMPLATE: &str = include_str!("../../templates/drivers/imu.rs.template");
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DriverType {
Camera,
Motor,
Sensor,
Imu,
}
impl DriverType {
pub const ALL: [DriverType; 4] = [
DriverType::Camera,
DriverType::Motor,
DriverType::Sensor,
DriverType::Imu,
];
pub fn as_str(self) -> &'static str {
match self {
DriverType::Camera => "camera",
DriverType::Motor => "motor",
DriverType::Sensor => "sensor",
DriverType::Imu => "imu",
}
}
pub fn parse(value: &str) -> Result<Self> {
DriverType::ALL
.into_iter()
.find(|kind| kind.as_str().eq_ignore_ascii_case(value))
.ok_or_else(|| {
anyhow::anyhow!(
"Unknown driver type '{}'. Available driver types: {}",
value,
DriverType::ALL.map(|kind| kind.as_str()).join(", ")
)
})
}
fn lib_rs_template(self) -> &'static str {
match self {
DriverType::Camera => CAMERA_LIB_RS_TEMPLATE,
DriverType::Motor => MOTOR_LIB_RS_TEMPLATE,
DriverType::Sensor => SENSOR_LIB_RS_TEMPLATE,
DriverType::Imu => IMU_LIB_RS_TEMPLATE,
}
}
}
pub struct DriverGeneratorService;
impl DriverGeneratorService {
pub fn new() -> Self {
Self
}
pub async fn generate_driver(&self, project_root: &Path, name: &str, driver_type: DriverType) -> Result<()> {
self.validate_driver_name(name)?;
let driver_dir = project_root.join(paths::project::DRIVERS_DIR).join(name);
if driver_dir.exists() {
anyhow::bail!("Driver '{}' already exists at {}", name, driver_dir.display());
}
let src_dir = driver_dir.join(paths::project::SRC_DIR);
tokio::fs::create_dir_all(&src_dir)
.await
.context("Failed to create driver directory")?;
let crate_name = self.to_snake_case(name);
let pascal_name = to_pascal_case(&crate_name);
let cargo_toml = render(CARGO_TOML_TEMPLATE, &[("name", name), ("crate_name", &crate_name)]);
tokio::fs::write(driver_dir.join(paths::rust::CARGO_TOML), cargo_toml).await?;
let main_rs = render(MAIN_RS_TEMPLATE, &[("name", name), ("crate_name", &crate_name)]);
tokio::fs::write(src_dir.join("main.rs"), main_rs).await?;
let lib_rs = render(
driver_type.lib_rs_template(),
&[("name", name), ("PascalName", &pascal_name)],
);
tokio::fs::write(src_dir.join("lib.rs"), lib_rs).await?;
add_workspace_member(project_root, &format!("{}/{}", paths::project::DRIVERS_DIR, name)).await?;
Ok(())
}
fn validate_driver_name(&self, name: &str) -> Result<()> {
crate::services::generator_name::validate_generator_name("Driver", name)
}
fn to_snake_case(&self, name: &str) -> String {
name.replace('-', "_")
}
}
impl Default for DriverGeneratorService {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
async fn setup_project() -> TempDir {
let temp = TempDir::new().unwrap();
tokio::fs::write(temp.path().join("Cargo.toml"), "[workspace]\nmembers = []\n")
.await
.unwrap();
temp
}
#[test]
fn driver_type_parse_accepts_all_known_types() {
assert_eq!(DriverType::parse("camera").unwrap(), DriverType::Camera);
assert_eq!(DriverType::parse("MOTOR").unwrap(), DriverType::Motor);
assert_eq!(DriverType::parse("Sensor").unwrap(), DriverType::Sensor);
assert_eq!(DriverType::parse("imu").unwrap(), DriverType::Imu);
}
#[test]
fn driver_type_parse_rejects_unknown_type_and_lists_available_ones() {
let err = DriverType::parse("gripper").unwrap_err();
assert!(err
.to_string()
.contains("Available driver types: camera, motor, sensor, imu"));
}
#[test]
fn validate_driver_name_rejects_leading_digit() {
let service = DriverGeneratorService::new();
let err = service.validate_driver_name("1camera").unwrap_err();
assert!(err.to_string().contains("must start with a letter"));
}
#[tokio::test]
async fn generate_driver_creates_expected_file_structure() {
let temp = setup_project().await;
let service = DriverGeneratorService::new();
service
.generate_driver(temp.path(), "my-camera", DriverType::Camera)
.await
.unwrap();
let driver_dir = temp.path().join("drivers/my-camera");
assert!(driver_dir.join("Cargo.toml").exists());
assert!(driver_dir.join("src/lib.rs").exists());
assert!(driver_dir.join("src/main.rs").exists());
}
#[tokio::test]
async fn generate_driver_fails_if_driver_already_exists() {
let temp = setup_project().await;
let service = DriverGeneratorService::new();
service
.generate_driver(temp.path(), "motor1", DriverType::Motor)
.await
.unwrap();
let err = service
.generate_driver(temp.path(), "motor1", DriverType::Motor)
.await
.unwrap_err();
assert!(err.to_string().contains("already exists"));
}
#[tokio::test]
async fn generate_driver_lib_rs_uses_pascal_case_and_type_specific_content() {
let temp = setup_project().await;
let service = DriverGeneratorService::new();
service
.generate_driver(temp.path(), "front-imu", DriverType::Imu)
.await
.unwrap();
let lib_rs = tokio::fs::read_to_string(temp.path().join("drivers/front-imu/src/lib.rs"))
.await
.unwrap();
assert!(lib_rs.contains("pub struct FrontImuNode"));
assert!(lib_rs.contains("FrontImuConfig"));
assert!(lib_rs.contains("Topic::new(\"/front-imu/data\")"));
assert!(lib_rs.contains("mecha10_core::node_main!(FrontImuNode, \"front-imu\");"));
assert!(lib_rs.contains("pub struct ImuData"));
}
#[tokio::test]
async fn generate_driver_updates_cargo_workspace_members() {
let temp = setup_project().await;
let service = DriverGeneratorService::new();
service
.generate_driver(temp.path(), "wheel-motor", DriverType::Motor)
.await
.unwrap();
let content = tokio::fs::read_to_string(temp.path().join("Cargo.toml")).await.unwrap();
let doc: toml::Value = toml::from_str(&content).unwrap();
let members = doc["workspace"]["members"].as_array().unwrap();
assert!(members.iter().any(|m| m.as_str() == Some("drivers/wheel-motor")));
}
#[tokio::test]
async fn generate_driver_does_not_touch_mecha10_json() {
let temp = setup_project().await;
let service = DriverGeneratorService::new();
service
.generate_driver(temp.path(), "bench-sensor", DriverType::Sensor)
.await
.unwrap();
assert!(!temp.path().join("mecha10.json").exists());
}
}