use std::collections::HashMap;
use crate::error::Result;
use crate::template::TronTemplate;
use crate::template_ref::TronRef;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Severity {
Info,
Warning,
Error,
}
impl std::fmt::Display for Severity {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Severity::Info => write!(f, "INFO"),
Severity::Warning => write!(f, "WARNING"),
Severity::Error => write!(f, "ERROR"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IssueCategory {
Syntax,
Placeholder,
Performance,
Style,
Security,
}
impl std::fmt::Display for IssueCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
IssueCategory::Syntax => write!(f, "Syntax"),
IssueCategory::Placeholder => write!(f, "Placeholder"),
IssueCategory::Performance => write!(f, "Performance"),
IssueCategory::Style => write!(f, "Style"),
IssueCategory::Security => write!(f, "Security"),
}
}
}
#[derive(Debug, Clone)]
pub struct ValidationIssue {
severity: Severity,
category: IssueCategory,
message: String,
line: Option<usize>,
column: Option<usize>,
suggestion: Option<String>,
}
impl ValidationIssue {
pub fn new<S: Into<String>>(severity: Severity, category: IssueCategory, message: S) -> Self {
Self {
severity,
category,
message: message.into(),
line: None,
column: None,
suggestion: None,
}
}
pub fn with_line(mut self, line: usize) -> Self {
self.line = Some(line);
self
}
pub fn with_column(mut self, column: usize) -> Self {
self.column = Some(column);
self
}
pub fn with_suggestion<S: Into<String>>(mut self, suggestion: S) -> Self {
self.suggestion = Some(suggestion.into());
self
}
pub fn severity(&self) -> Severity {
self.severity
}
pub fn category(&self) -> &IssueCategory {
&self.category
}
pub fn message(&self) -> &str {
&self.message
}
pub fn line(&self) -> Option<usize> {
self.line
}
pub fn column(&self) -> Option<usize> {
self.column
}
pub fn suggestion(&self) -> Option<&str> {
self.suggestion.as_deref()
}
}
impl std::fmt::Display for ValidationIssue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}: {}", self.severity, self.category, self.message)?;
if let (Some(line), Some(column)) = (self.line, self.column) {
write!(f, " (line {}, column {})", line, column)?;
} else if let Some(line) = self.line {
write!(f, " (line {})", line)?;
}
if let Some(suggestion) = &self.suggestion {
write!(f, "\n Suggestion: {}", suggestion)?;
}
Ok(())
}
}
#[derive(Debug)]
pub struct ValidationReport {
issues: Vec<ValidationIssue>,
template_path: Option<String>,
}
impl ValidationReport {
pub fn new() -> Self {
Self {
issues: Vec::new(),
template_path: None,
}
}
pub fn with_path<S: Into<String>>(mut self, path: S) -> Self {
self.template_path = Some(path.into());
self
}
pub fn add_issue(&mut self, issue: ValidationIssue) {
self.issues.push(issue);
}
pub fn has_issues(&self) -> bool {
!self.issues.is_empty()
}
pub fn has_errors(&self) -> bool {
self.issues.iter().any(|issue| issue.severity == Severity::Error)
}
pub fn issues(&self) -> &[ValidationIssue] {
&self.issues
}
pub fn issues_by_severity(&self, severity: Severity) -> Vec<&ValidationIssue> {
self.issues.iter()
.filter(|issue| issue.severity == severity)
.collect()
}
pub fn issues_by_category(&self, category: &IssueCategory) -> Vec<&ValidationIssue> {
self.issues.iter()
.filter(|issue| &issue.category == category)
.collect()
}
pub fn template_path(&self) -> Option<&str> {
self.template_path.as_deref()
}
pub fn count_by_severity(&self) -> HashMap<Severity, usize> {
let mut counts = HashMap::new();
for issue in &self.issues {
*counts.entry(issue.severity).or_insert(0) += 1;
}
counts
}
}
impl Default for ValidationReport {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for ValidationReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(path) = &self.template_path {
writeln!(f, "Validation Report for: {}", path)?;
} else {
writeln!(f, "Validation Report:")?;
}
if self.issues.is_empty() {
writeln!(f, "✓ No issues found")?;
} else {
let counts = self.count_by_severity();
writeln!(f, "Found {} issue(s):", self.issues.len())?;
for (severity, count) in counts {
writeln!(f, " {} {}: {}",
match severity {
Severity::Error => "❌",
Severity::Warning => "⚠️",
Severity::Info => "ℹ️",
},
severity, count)?;
}
writeln!(f)?;
for (index, issue) in self.issues.iter().enumerate() {
writeln!(f, "{}. {}", index + 1, issue)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone)]
pub struct ValidationConfig {
pub check_placeholder_naming: bool,
pub min_placeholder_length: usize,
pub check_unused_placeholders: bool,
pub check_security: bool,
pub check_performance: bool,
pub max_nesting_depth: usize,
}
impl Default for ValidationConfig {
fn default() -> Self {
Self {
check_placeholder_naming: true,
min_placeholder_length: 2,
check_unused_placeholders: true,
check_security: true,
check_performance: true,
max_nesting_depth: 10,
}
}
}
#[derive(Debug)]
pub struct TemplateValidator {
config: ValidationConfig,
}
impl TemplateValidator {
pub fn new() -> Self {
Self {
config: ValidationConfig::default(),
}
}
pub fn with_config(config: ValidationConfig) -> Self {
Self { config }
}
pub fn validate(&self, template: &TronTemplate) -> Result<ValidationReport> {
let mut report = ValidationReport::new();
if let Some(path) = template.path() {
report = report.with_path(path.to_string_lossy().to_string());
}
self.check_placeholder_issues(template, &mut report)?;
self.check_style_issues(template, &mut report)?;
if self.config.check_security {
self.check_security_issues(template, &mut report)?;
}
if self.config.check_performance {
self.check_performance_issues(template, &mut report)?;
}
Ok(report)
}
pub fn validate_ref(&self, template_ref: &TronRef) -> Result<ValidationReport> {
self.validate(template_ref.inner())
}
pub fn check_circular_references(&self, templates: &[&TronTemplate]) -> Result<ValidationReport> {
let mut report = ValidationReport::new();
let mut dependency_graph = HashMap::new();
for template in templates {
let placeholders = template.placeholder_names();
let content = template.content();
for placeholder in placeholders {
if content.contains(&format!("@[{}]@", placeholder)) {
dependency_graph.entry(template.content().clone())
.or_insert_with(Vec::new)
.push(placeholder);
}
}
}
if dependency_graph.len() > 1 {
report.add_issue(ValidationIssue::new(
Severity::Info,
IssueCategory::Performance,
"Multiple templates detected - consider checking for circular references manually"
));
}
Ok(report)
}
fn check_placeholder_issues(&self, template: &TronTemplate, report: &mut ValidationReport) -> Result<()> {
let placeholders = template.placeholder_names();
if self.config.check_placeholder_naming {
for placeholder in &placeholders {
if placeholder.len() < self.config.min_placeholder_length {
report.add_issue(ValidationIssue::new(
Severity::Warning,
IssueCategory::Style,
format!("Placeholder '{}' is shorter than recommended minimum of {} characters",
placeholder, self.config.min_placeholder_length)
).with_suggestion("Consider using more descriptive placeholder names"));
}
if placeholder.chars().any(|c| c.is_uppercase()) {
report.add_issue(ValidationIssue::new(
Severity::Warning,
IssueCategory::Style,
format!("Placeholder '{}' contains uppercase letters", placeholder)
).with_suggestion("Consider using snake_case for placeholder names"));
}
if placeholder.chars().all(|c| c.is_numeric()) {
report.add_issue(ValidationIssue::new(
Severity::Warning,
IssueCategory::Style,
format!("Placeholder '{}' consists only of numbers", placeholder)
).with_suggestion("Consider using descriptive names instead of just numbers"));
}
if placeholder.chars().any(char::is_whitespace) || !placeholder.chars().all(|c| c == '_' || c.is_ascii_alphanumeric()) {
report.add_issue(ValidationIssue::new(
Severity::Error,
IssueCategory::Syntax,
format!("Placeholder '{}' contains invalid characters (whitespace or non-identifier)", placeholder)
).with_suggestion("Use only letters, numbers, and underscores in placeholder names"));
}
}
}
let content = template.content();
if content.contains("@@") {
report.add_issue(ValidationIssue::new(
Severity::Error,
IssueCategory::Syntax,
"Found '@@' which might indicate a malformed placeholder"
).with_suggestion("Check for missing brackets in placeholder syntax"));
}
Ok(())
}
fn check_style_issues(&self, template: &TronTemplate, report: &mut ValidationReport) -> Result<()> {
let content = template.content();
for (line_num, line) in content.lines().enumerate() {
if line.ends_with(' ') || line.ends_with('\t') {
report.add_issue(ValidationIssue::new(
Severity::Warning,
IssueCategory::Style,
"Line has trailing whitespace"
).with_line(line_num + 1)
.with_suggestion("Remove trailing whitespace"));
}
}
let has_tabs = content.contains('\t');
let has_space_indent = content.lines().any(|line| line.starts_with(" "));
if has_tabs && has_space_indent {
report.add_issue(ValidationIssue::new(
Severity::Warning,
IssueCategory::Style,
"Inconsistent indentation detected (mixing tabs and spaces)"
).with_suggestion("Use consistent indentation throughout the template"));
}
Ok(())
}
fn check_security_issues(&self, template: &TronTemplate, report: &mut ValidationReport) -> Result<()> {
let content = template.content();
let suspicious_patterns = [
"eval(",
"exec(",
"system(",
"shell_exec(",
"<script",
"javascript:",
];
for pattern in &suspicious_patterns {
if content.to_lowercase().contains(pattern) {
report.add_issue(ValidationIssue::new(
Severity::Warning,
IssueCategory::Security,
format!("Found potentially unsafe pattern: '{}'", pattern)
).with_suggestion("Review for potential security implications"));
}
}
let secret_keywords = ["password", "api_key", "apikey", "secret", "token"];
let lower_content = content.to_lowercase();
for keyword in &secret_keywords {
if lower_content.contains(&format!("{}=", keyword)) ||
lower_content.contains(&format!("{} =", keyword)) {
report.add_issue(ValidationIssue::new(
Severity::Warning,
IssueCategory::Security,
format!("Found potential hardcoded secret with keyword '{}'", keyword)
).with_suggestion("Consider using placeholders for sensitive values"));
}
}
Ok(())
}
fn check_performance_issues(&self, template: &TronTemplate, report: &mut ValidationReport) -> Result<()> {
let content = template.content();
let placeholder_count = template.placeholder_names().len();
if placeholder_count > 50 {
report.add_issue(ValidationIssue::new(
Severity::Warning,
IssueCategory::Performance,
format!("Template has {} placeholders, which might impact performance", placeholder_count)
).with_suggestion("Consider breaking down into smaller templates or using template composition"));
}
if content.len() > 10000 {
report.add_issue(ValidationIssue::new(
Severity::Info,
IssueCategory::Performance,
format!("Large template size ({} characters)", content.len())
).with_suggestion("Consider breaking down into smaller, more manageable templates"));
}
if content.contains("@[") && (content.contains("*") || content.contains("+")) {
report.add_issue(ValidationIssue::new(
Severity::Info,
IssueCategory::Performance,
"Template contains both placeholders and regex-like characters"
).with_suggestion("Ensure placeholder names don't contain complex regex patterns"));
}
Ok(())
}
}
impl Default for TemplateValidator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validation_issue_creation() {
let issue = ValidationIssue::new(
Severity::Warning,
IssueCategory::Style,
"Test issue"
).with_line(5)
.with_suggestion("Fix it");
assert_eq!(issue.severity(), Severity::Warning);
assert_eq!(issue.category(), &IssueCategory::Style);
assert_eq!(issue.message(), "Test issue");
assert_eq!(issue.line(), Some(5));
assert_eq!(issue.suggestion(), Some("Fix it"));
}
#[test]
fn test_validation_report() {
let mut report = ValidationReport::new();
assert!(!report.has_issues());
assert!(!report.has_errors());
report.add_issue(ValidationIssue::new(
Severity::Warning,
IssueCategory::Style,
"Warning"
));
assert!(report.has_issues());
assert!(!report.has_errors());
report.add_issue(ValidationIssue::new(
Severity::Error,
IssueCategory::Syntax,
"Error"
));
assert!(report.has_errors());
assert_eq!(report.issues().len(), 2);
}
#[test]
fn test_template_validation() -> Result<()> {
let template = TronTemplate::new("Hello @[n]@!")?;
let validator = TemplateValidator::new();
let report = validator.validate(&template)?;
assert!(report.has_issues());
let warnings = report.issues_by_severity(Severity::Warning);
assert!(!warnings.is_empty());
Ok(())
}
#[test]
fn test_good_template_validation() -> Result<()> {
let template = TronTemplate::new("Hello @[user_name]@! Welcome to @[application_name]@.")?;
let validator = TemplateValidator::new();
let report = validator.validate(&template)?;
assert!(!report.has_errors());
Ok(())
}
#[test]
fn test_security_validation() -> Result<()> {
let template = TronTemplate::new("function test() { eval(@[code]@); }")?;
let validator = TemplateValidator::new();
let report = validator.validate(&template)?;
let security_issues = report.issues_by_category(&IssueCategory::Security);
assert!(!security_issues.is_empty());
Ok(())
}
#[test]
fn test_custom_config() -> Result<()> {
let mut config = ValidationConfig::default();
config.min_placeholder_length = 5;
let template = TronTemplate::new("Hello @[name]@!")?; let validator = TemplateValidator::with_config(config);
let report = validator.validate(&template)?;
assert!(report.has_issues());
Ok(())
}
}