use crate::error::{Error, Result};
use crate::templates::manifest::TemplateManifest;
use std::path::Path;
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl ValidationResult {
pub fn new() -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
pub fn add_error(&mut self, message: impl Into<String>) {
self.errors.push(message.into());
self.valid = false;
}
pub fn add_warning(&mut self, message: impl Into<String>) {
self.warnings.push(message.into());
}
}
impl Default for ValidationResult {
fn default() -> Self {
Self::new()
}
}
pub async fn validate_template(template_dir: &Path, manifest: &TemplateManifest) -> Result<()> {
let result = validate_template_detailed(template_dir, manifest).await;
if !result.valid {
let error_msg = format!("Template validation failed:\n{}", result.errors.join("\n"));
return Err(Error::template(error_msg));
}
for warning in &result.warnings {
tracing::warn!("Template warning: {}", warning);
}
Ok(())
}
pub async fn validate_template_detailed(
template_dir: &Path,
manifest: &TemplateManifest,
) -> ValidationResult {
let mut result = ValidationResult::new();
validate_manifest_fields(manifest, &mut result);
validate_template_files(template_dir, manifest, &mut result).await;
validate_template_structure(template_dir, &mut result).await;
validate_ferrous_forge_compliance(template_dir, manifest, &mut result).await;
result
}
fn validate_manifest_fields(manifest: &TemplateManifest, result: &mut ValidationResult) {
if manifest.name.is_empty() {
result.add_error("Template name cannot be empty");
}
if manifest.version.is_empty() {
result.add_error("Template version cannot be empty");
} else if !is_valid_version(&manifest.version) {
result.add_warning(format!(
"Version '{}' does not follow semantic versioning",
manifest.version
));
}
if manifest.description.is_empty() {
result.add_warning("Template description is empty");
}
if manifest.author.is_empty() {
result.add_warning("Template author is empty");
}
let valid_editions = ["2015", "2018", "2021", "2024"];
if !valid_editions.contains(&manifest.edition.as_str()) {
result.add_warning(format!("Edition '{}' may not be valid", manifest.edition));
}
let mut seen_names = std::collections::HashSet::new();
for var in &manifest.variables {
if !seen_names.insert(&var.name) {
result.add_error(format!("Duplicate variable name: {}", var.name));
}
if var.name == "project_name" || var.name == "author" {
result.add_warning(format!(
"Variable '{}' may conflict with default variables",
var.name
));
}
}
if manifest.files.is_empty() {
result.add_error("Template must have at least one file");
}
}
async fn validate_template_files(
template_dir: &Path,
manifest: &TemplateManifest,
result: &mut ValidationResult,
) {
for file in &manifest.files {
let source_path = template_dir.join(&file.source);
if !source_path.exists() {
result.add_error(format!(
"Template file not found: {}",
file.source.display()
));
}
}
let required_files = ["template.toml"];
for required in &required_files {
let path = template_dir.join(required);
if !path.exists() {
result.add_error(format!("Required file missing: {}", required));
}
}
}
async fn validate_template_structure(template_dir: &Path, result: &mut ValidationResult) {
let cargo_toml = template_dir.join("Cargo.toml");
if !cargo_toml.exists() {
result.add_warning("No Cargo.toml found - template may not be a Rust project");
} else {
match tokio::fs::read_to_string(&cargo_toml).await {
Ok(content) => {
if let Err(e) = toml::from_str::<toml::Value>(&content) {
result.add_error(format!("Invalid Cargo.toml: {}", e));
}
}
Err(e) => {
result.add_error(format!("Failed to read Cargo.toml: {}", e));
}
}
}
let src_dir = template_dir.join("src");
if !src_dir.exists() {
result.add_warning("No src/ directory found");
}
}
async fn validate_ferrous_forge_compliance(
template_dir: &Path,
manifest: &TemplateManifest,
result: &mut ValidationResult,
) {
let forge_config = template_dir.join(".ferrous-forge").join("config.toml");
if !forge_config.exists() {
result.add_warning("Template does not include Ferrous Forge configuration");
}
let ci_dirs = [".github/workflows", ".ci"];
let has_ci = ci_dirs.iter().any(|dir| template_dir.join(dir).exists());
if !has_ci {
result.add_warning("Template does not include CI configuration");
}
if manifest.edition != "2024" {
result.add_warning(format!(
"Template uses edition {} instead of recommended 2024",
manifest.edition
));
}
}
fn is_valid_version(version: &str) -> bool {
let parts: Vec<&str> = version.split('.').collect();
if parts.len() < 2 || parts.len() > 3 {
return false;
}
parts.iter().all(|p| p.parse::<u64>().is_ok())
}
pub async fn validate_before_install(template_dir: &Path) -> Result<ValidationResult> {
let manifest_path = template_dir.join("template.toml");
if !manifest_path.exists() {
return Err(Error::template(
"Template manifest not found: template.toml",
));
}
let manifest_content = tokio::fs::read_to_string(&manifest_path)
.await
.map_err(|e| Error::template(format!("Failed to read manifest: {e}")))?;
let manifest: TemplateManifest = toml::from_str(&manifest_content)
.map_err(|e| Error::template(format!("Failed to parse manifest: {e}")))?;
let result = validate_template_detailed(template_dir, &manifest).await;
Ok(result)
}