#![cfg_attr(test, allow(clippy::disallowed_methods))]
use color_eyre::Result;
use std::{fmt, path::PathBuf};
pub mod template;
pub mod tools;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ProjectType {
Binary,
Library,
}
impl fmt::Display for ProjectType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProjectType::Binary => write!(f, "Binary application"),
ProjectType::Library => write!(f, "Library crate"),
}
}
}
#[derive(Debug)]
pub struct ProjectConfig {
pub name: String,
pub project_type: ProjectType,
pub edition: String,
pub license: String,
pub git: bool,
pub path: PathBuf,
pub yes: bool,
}
pub fn find_templates_dir() -> Result<PathBuf, std::io::Error> {
let mut dir = std::env::current_dir()?;
loop {
let candidate = dir.join("templates");
if candidate.is_dir() {
return Ok(candidate);
}
if !dir.pop() {
break;
}
}
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not find a 'templates/' directory in this or any parent directory.",
))
}
pub fn generate_project(config: ProjectConfig) -> Result<()> {
use template::{TemplateEngine, TemplateLoader, TemplateVariables, TemplateVariant};
if let Some(parent) = config.path.parent() {
if !parent.exists() {
return Err(color_eyre::eyre::eyre!(
"Parent directory '{}' does not exist. Please create it first.",
parent.display()
));
}
}
let variables = TemplateVariables::from_config(&config);
let engine = TemplateEngine::new(variables);
let template_path = find_templates_dir()?;
let loader = TemplateLoader::new(template_path);
let variant = TemplateVariant::Extended;
let templates = loader.list_templates(config.project_type, variant)?;
std::fs::create_dir_all(&config.path)?;
for template_path in templates {
let rel_path = pathdiff::diff_paths(&template_path, loader.base_path())
.unwrap_or_else(|| template_path.clone());
let rel_path_str = rel_path.to_string_lossy();
let template_content = loader.load_template(&rel_path_str)?;
let rendered = engine.render_template(&template_content)?;
let output_path = loader.get_destination_path(&template_path, &config.path);
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(output_path, rendered)?;
}
if config.git {
}
println!("Successfully generated project: {}", config.name);
Ok(())
}
#[derive(Debug)]
pub struct Config {
pub name: String,
pub bin: bool,
pub lib: bool,
pub edition: String,
pub license: String,
pub git: bool,
pub path: PathBuf,
pub yes: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_project_type_display() {
assert_eq!(ProjectType::Binary.to_string(), "Binary application");
assert_eq!(ProjectType::Library.to_string(), "Library crate");
}
#[test]
fn test_find_templates_dir_error() {
if cfg!(miri) {
eprintln!("Skipping file system test under Miri");
return;
}
let dir = tempdir().unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(dir.path()).unwrap();
let result = find_templates_dir();
std::env::set_current_dir(prev).unwrap();
assert!(result.is_err());
}
#[test]
fn test_config_struct_instantiation() {
let config = Config {
name: "foo".to_string(),
bin: true,
lib: false,
edition: "2021".to_string(),
license: "MIT".to_string(),
git: true,
path: PathBuf::from("/tmp/foo"),
yes: false,
};
assert_eq!(config.name, "foo");
assert!(config.bin);
}
#[test]
fn test_project_config_edge_cases() {
let config = ProjectConfig {
name: "".to_string(),
project_type: ProjectType::Library,
edition: "2015".to_string(),
license: "GPL-3.0".to_string(),
git: false,
path: PathBuf::from("/tmp/empty"),
yes: true,
};
assert_eq!(config.name, "");
match config.project_type {
ProjectType::Library => {}
_ => panic!("Expected Library variant"),
}
assert_eq!(config.edition, "2015");
assert_eq!(config.license, "GPL-3.0");
assert!(!config.git);
assert!(config.yes);
}
#[test]
fn test_generate_project_template_error() {
let _nonexistent_path = PathBuf::from("/path/that/definitely/does/not/exist/templates");
let template_error = std::io::Error::new(
std::io::ErrorKind::NotFound,
"Could not find a 'templates/' directory in this or any parent directory.",
);
let result: Result<(), template::TemplateError> = Err(template::TemplateError::LoadError {
path: "templates".to_string(),
source: template_error,
});
assert!(result.is_err(), "Should error if templates/ dir is missing");
if let Err(e) = result {
assert!(
e.to_string()
.contains("Could not find a 'templates/' directory"),
"Error should mention missing templates directory, got: {e}"
);
}
}
#[test]
fn test_generate_project_write_error() {
if cfg!(miri) {
eprintln!("Skipping file system test under Miri");
return;
}
let test_dir = tempfile::tempdir().unwrap();
let test_path = test_dir.path();
let output_file = test_path.join("output_file");
fs::write(&output_file, "not a dir").unwrap();
let templates_dir = test_path.join("templates");
let base_dir = templates_dir.join("base");
let binary_dir = templates_dir.join("binary");
let binary_extended_dir = binary_dir.join("extended");
let binary_minimal_dir = binary_dir.join("minimal");
let library_dir = templates_dir.join("library");
let library_extended_dir = library_dir.join("extended");
let library_minimal_dir = library_dir.join("minimal");
fs::create_dir_all(&base_dir).unwrap();
fs::create_dir_all(&binary_extended_dir).unwrap();
fs::create_dir_all(&binary_minimal_dir).unwrap();
fs::create_dir_all(&library_extended_dir).unwrap();
fs::create_dir_all(&library_minimal_dir).unwrap();
fs::write(
base_dir.join("README.md.hbs"),
"# {{name}}\n\nThis is a test project.",
)
.unwrap();
fs::write(
binary_extended_dir.join("main.rs.hbs"),
"fn main() {\n println!(\"Hello from {{name}}!\");\n}",
)
.unwrap();
fs::write(
binary_minimal_dir.join("main.rs.hbs"),
"fn main() {\n println!(\"Minimal {{name}}!\");\n}",
)
.unwrap();
fs::write(
library_extended_dir.join("lib.rs.hbs"),
"pub fn hello() {\n println!(\"Hello from {{name}} library!\");\n}",
)
.unwrap();
fs::write(
library_minimal_dir.join("lib.rs.hbs"),
"pub fn hello() {}\n",
)
.unwrap();
let prev = std::env::current_dir().unwrap();
std::env::set_current_dir(test_path).unwrap();
let config = ProjectConfig {
name: "test-project".to_string(),
project_type: ProjectType::Binary,
edition: "2021".to_string(),
license: "MIT".to_string(),
git: false,
path: output_file,
yes: true,
};
let result = generate_project(config);
std::env::set_current_dir(prev).unwrap();
assert!(result.is_err(), "Should error when output path is a file");
if let Err(e) = result {
assert!(
e.to_string().contains("Not a directory")
|| e.to_string().contains("Is a file")
|| e.to_string().contains("already exists")
|| e.to_string().contains("Permission denied")
|| e.to_string().contains("File exists"),
"Error should be about output path being a file, got: {e}"
);
}
}
}