use crate::error::{Result, ZahuyachError};
use crate::templates;
use std::{env, fs, path::PathBuf};
pub fn run(name: Option<&String>) -> Result<String> {
let project_path = determine_project_path(name)?;
let project_name = project_path
.file_name()
.and_then(|name| name.to_str())
.ok_or_else(|| {
ZahuyachError::InvalidInput("Cannot determine project name from path".to_string())
})?;
validate_project_name(&project_name)?;
if name.is_some() {
if project_path.exists() {
let is_empty = fs::read_dir(&project_path)
.map_err(|e| ZahuyachError::Io(e))?
.next()
.is_none();
if !is_empty {
return Err(ZahuyachError::InvalidInput(format!(
"Directory '{}' already exists and is not empty",
project_path.display()
)));
}
}
} else {
let is_empty = fs::read_dir(&project_path)
.map_err(|e| ZahuyachError::Io(e))?
.next()
.is_none();
if !is_empty {
return Err(ZahuyachError::InvalidInput(
"Current directory is not empty. Please run in an empty directory or specify a project name.".to_string()
));
}
}
templates::copy_basic_template(&project_path)?;
Ok(format!(
"Successfully initialized new blog project: {} in directory {}",
project_name,
project_path.display()
))
}
fn determine_project_path(name: Option<&String>) -> Result<PathBuf> {
let current_dir = env::current_dir().map_err(|e| ZahuyachError::Io(e))?;
match name {
Some(name) => Ok(current_dir.join(name)),
None => Ok(current_dir),
}
}
fn validate_project_name(name: &str) -> Result<()> {
if name.is_empty() {
return Err(ZahuyachError::InvalidInput(
"Project name cannot be empty".to_string(),
));
}
let invalid_chars: &[char] = &['/', '\\', ':', '*', '?', '"', '<', '>', '|'];
if name.contains(invalid_chars) {
return Err(ZahuyachError::InvalidInput(format!(
"Project name cannot contain invalid characters '{}'",
invalid_chars.iter().collect::<String>()
)));
}
if name.starts_with('.') || name.starts_with('-') {
return Err(ZahuyachError::InvalidInput(
"Project name cannot start with a dot '.' or hyphen '-'.".to_string(),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
#[rstest]
#[case("my-blog", true)]
#[case("blog123", true)]
#[case("my_blog", true)]
#[case("", false)]
#[case(".hidden", false)]
#[case("-invalid", false)]
#[case("with/slash", false)]
#[case("with\\backslash", false)]
#[case("with:colon", false)]
#[case("with*asterisk", false)]
#[case("with?question", false)]
#[case("with\"quote", false)]
#[case("with<bracket", false)]
#[case("with>bracket", false)]
#[case("with|pipe", false)]
fn test_validate_project_name(#[case] name: &str, #[case] should_be_valid: bool) {
let result = validate_project_name(name);
if should_be_valid {
assert!(result.is_ok(), "Expected '{}' to be valid", name);
} else {
assert!(result.is_err(), "Expected '{}' to be invalid", name);
}
}
#[rstest]
#[case(Some("test-blog".to_string()), "test-blog")]
fn test_determine_project_path_with_input(
#[case] input: Option<String>,
#[case] expected_name: &str,
) {
let result = determine_project_path(input.as_ref()).unwrap();
let current_dir = std::env::current_dir().unwrap();
let expected_path = current_dir.join(expected_name);
assert_eq!(result, expected_path);
}
#[test]
fn test_determine_project_path_without_input() {
let expected = std::env::current_dir().unwrap();
let result = determine_project_path(None).unwrap();
assert_eq!(result, expected);
}
}