use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
const CARGO_TOML_FILE_NAME: &str = "Cargo.toml";
const DEFAULT_BIN_NAME: &str = "bin";
const DEFAULT_MAIN_PATH: &str = "src/main.rs";
#[derive(Debug, Serialize, Deserialize)]
pub struct CargoToml {
package: PackageConfig,
dependencies: BTreeMap<String, toml::Value>,
bin: Vec<BinConfig>,
profile: ProfileConfig,
}
impl CargoToml {
pub fn new(
name: &String,
feature: &String,
hybrid: &bool,
user_dependencies: Option<&BTreeMap<String, toml::Value>>
) -> CargoToml {
let mut deps = BTreeMap::new();
deps.insert("panic-halt".to_string(), toml::Value::String("1.0.0".to_string()));
deps.insert("ufmt".to_string(), toml::Value::String("0.2.0".to_string()));
deps.insert("nb".to_string(), toml::Value::String("1.1.0".to_string()));
deps.insert("embedded-hal".to_string(), toml::Value::String("1.0".to_string()));
let mut hal_map = toml::map::Map::new();
hal_map.insert("git".to_string(), toml::Value::String("https://github.com/rahix/avr-hal".to_string()));
hal_map.insert("rev".to_string(), toml::Value::String("e5c8f37fe48419956e722490a82b9ca9b9fc61a2".to_string()));
hal_map.insert("features".to_string(), toml::Value::Array(vec![toml::Value::String(feature.clone())]));
deps.insert("arduino-hal".to_string(), toml::Value::Table(hal_map));
if *hybrid {
let mut prustio_map = toml::map::Map::new();
prustio_map.insert("git".to_string(), toml::Value::String("https://github.com/MikiiN/prustio-arduino-crate".to_string()));
deps.insert("prustio-arduino".to_string(), toml::Value::Table(prustio_map));
}
if let Some(user_deps) = user_dependencies {
for (key, val) in user_deps {
deps.insert(key.clone(), val.clone());
}
}
CargoToml {
package: PackageConfig::new(name),
dependencies: deps,
bin: vec![BinConfig::new()],
profile: ProfileConfig::new(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PackageConfig {
pub name: String,
pub version: String,
pub edition: String,
}
impl PackageConfig {
pub fn new(name: &String) -> PackageConfig{
PackageConfig {
name: name.clone(),
version: "0.1.0".to_string(),
edition: "2024".to_string(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BinConfig {
pub name: String,
pub path: String,
pub test: bool,
pub bench: bool,
}
impl BinConfig {
pub fn new() -> BinConfig {
BinConfig {
name: DEFAULT_BIN_NAME.to_string(),
path: DEFAULT_MAIN_PATH.to_string(),
test: false,
bench: false,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ProfileConfig {
pub dev: ProfileDevConfig,
pub release: ProfileReleaseConfig,
}
impl ProfileConfig {
pub fn new() -> ProfileConfig {
ProfileConfig {
dev: ProfileDevConfig::new(),
release: ProfileReleaseConfig::new(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ProfileDevConfig {
pub panic: String,
pub lto: bool,
#[serde(rename = "opt-level")]
pub opt_level: String,
}
impl ProfileDevConfig {
pub fn new() -> ProfileDevConfig {
ProfileDevConfig {
panic: "abort".to_string(),
lto: true,
opt_level: "s".to_string(),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ProfileReleaseConfig {
pub panic: String,
#[serde(rename = "codegen-units")]
pub codegen_units: u32,
pub debug: bool,
pub lto: bool,
#[serde(rename = "opt-level")]
pub opt_level: String,
}
impl ProfileReleaseConfig {
pub fn new() -> ProfileReleaseConfig {
ProfileReleaseConfig {
panic: "abort".to_string(),
codegen_units: 1,
debug: true,
lto: true,
opt_level: "s".to_string(),
}
}
}
pub fn create_cargo_toml_config(
proj_path: &PathBuf,
project_name: &String,
board_feature: &String,
hybrid: &bool,
user_dependencies: Option<&BTreeMap<String, toml::Value>>,
) -> Result<(), String> {
let file_path = PathBuf::from(proj_path).join(CARGO_TOML_FILE_NAME);
let config = CargoToml::new(project_name, board_feature, hybrid, user_dependencies);
let content = match toml::to_string_pretty(&config) {
Ok(c) => c,
Err(_) => {
return Err("Failed to parse Cargo.toml configuration".to_string());
}
};
if let Err(_) = fs::write(&file_path, &content) {
return Err("Failed to write updated Cargo.toml file.".to_string());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_cargo_toml_generation_pure_mode() {
let cargo = CargoToml::new(&"pure_app".to_string(), &"arduino-uno".to_string(), &false, None);
assert_eq!(cargo.package.name, "pure_app");
assert!(cargo.dependencies.get("prustio-arduino").is_none());
let hal = cargo.dependencies.get("arduino-hal").unwrap().as_table().unwrap();
let features = hal.get("features").unwrap().as_array().unwrap();
assert_eq!(features[0].as_str().unwrap(), "arduino-uno");
}
#[test]
fn test_cargo_toml_generation_hybrid_mode() {
let cargo = CargoToml::new(&"hybrid_app".to_string(), &"arduino-mega2560".to_string(), &true, None);
assert!(cargo.dependencies.get("prustio-arduino").is_some());
let hal = cargo.dependencies.get("arduino-hal").unwrap().as_table().unwrap();
let features = hal.get("features").unwrap().as_array().unwrap();
assert_eq!(features[0].as_str().unwrap(), "arduino-mega2560");
}
#[test]
fn test_cargo_toml_with_user_dependencies() {
let mut user_deps = BTreeMap::new();
user_deps.insert("serde".to_string(), toml::Value::String("1.0".to_string()));
let mut req_map = toml::map::Map::new();
req_map.insert("version".to_string(), toml::Value::String("0.2".to_string()));
user_deps.insert("reqwest".to_string(), toml::Value::Table(req_map));
let cargo = CargoToml::new(&"deps_app".to_string(), &"arduino-uno".to_string(), &false, Some(&user_deps));
assert_eq!(cargo.dependencies.get("serde").unwrap().as_str().unwrap(), "1.0");
assert!(cargo.dependencies.get("reqwest").unwrap().is_table());
assert!(cargo.dependencies.get("panic-halt").is_some());
}
#[test]
fn test_bin_config_defaults() {
let bin = BinConfig::new();
assert_eq!(bin.name, DEFAULT_BIN_NAME);
assert_eq!(bin.path, DEFAULT_MAIN_PATH);
assert!(!bin.test);
assert!(!bin.bench);
}
#[test]
fn test_profile_config_defaults() {
let profile = ProfileConfig::new();
assert_eq!(profile.dev.panic, "abort");
assert!(profile.dev.lto);
assert_eq!(profile.dev.opt_level, "s");
assert_eq!(profile.release.panic, "abort");
assert_eq!(profile.release.codegen_units, 1);
assert!(profile.release.debug);
assert!(profile.release.lto);
assert_eq!(profile.release.opt_level, "s");
}
#[test]
fn test_create_cargo_toml_config_success() {
let temp_dir = tempdir().unwrap();
let proj_path = temp_dir.path().to_path_buf();
let result = create_cargo_toml_config(
&proj_path,
&"my_test_app".to_string(),
&"arduino-nano".to_string(),
&false,
None
);
assert!(result.is_ok());
let file_path = proj_path.join(CARGO_TOML_FILE_NAME);
assert!(file_path.exists());
let content = fs::read_to_string(file_path).expect("Failed to read generated Cargo.toml");
assert!(content.contains("name = \"my_test_app\""));
assert!(content.contains("arduino-nano"));
assert!(content.contains("panic-halt"));
assert!(content.contains("codegen-units = 1"));
}
#[test]
fn test_create_cargo_toml_config_failure() {
let proj_path = PathBuf::from("/invalid/path/that/does/not/exist");
let result = create_cargo_toml_config(
&proj_path,
&"my_test_app".to_string(),
&"arduino-nano".to_string(),
&false,
None
);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Failed to write updated Cargo.toml file.");
}
}