use std::path::Path;
use thiserror::Error;
pub trait TemplateProvider: Send + Sync {
fn discover(&self, path: &Path) -> Result<Vec<Template>, ProviderError>;
fn validate(&self, template: &Template) -> Result<(), ProviderError>;
fn render(&self, template: &Template, context: Context) -> Result<String, ProviderError>;
fn metadata(&self, template: &Template) -> Result<TemplateMetadata, ProviderError>;
fn version(&self) -> Version {
Version::new(1, 0, 0)
}
}
pub trait CliBridge: Send + Sync {
fn parse(&self, args: Vec<String>) -> Result<Command, BridgeError>;
fn execute<P: TemplateProvider>(
&self,
cmd: Command,
provider: &P,
) -> Result<Output, BridgeError>;
fn format(&self, output: Output) -> String;
fn version(&self) -> Version {
Version::new(1, 0, 0)
}
}
pub trait RenderEngine: Send + Sync {
fn render(&self, content: &str, context: &Context) -> Result<String, RenderError>;
fn validate_syntax(&self, content: &str) -> Result<(), RenderError>;
fn features(&self) -> RenderFeatures;
}
#[derive(Debug, Clone)]
pub struct Template {
pub name: String,
pub path: String,
pub content: String,
}
#[derive(Debug, Clone)]
pub struct TemplateMetadata {
pub name: String,
pub version: Version,
pub author: Option<String>,
pub description: Option<String>,
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Context {
pub variables: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct Command {
pub action: CommandAction,
pub args: Vec<String>,
pub flags: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub enum CommandAction {
Generate,
List,
Validate,
Search,
}
#[derive(Debug, Clone)]
pub struct Output {
pub success: bool,
pub message: String,
pub data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Version {
pub major: u32,
pub minor: u32,
pub patch: u32,
}
impl Version {
pub fn new(major: u32, minor: u32, patch: u32) -> Self {
Self {
major,
minor,
patch,
}
}
pub fn is_compatible_with(&self, other: &Version) -> bool {
self.major == other.major && self >= other
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Debug, Clone)]
pub struct RenderFeatures {
pub supports_partials: bool,
pub supports_helpers: bool,
pub supports_filters: bool,
pub supports_inheritance: bool,
}
#[derive(Error, Debug)]
pub enum ProviderError {
#[error("Template not found: {path}")]
TemplateNotFound { path: String },
#[error("Invalid template syntax: {reason}")]
InvalidSyntax { reason: String },
#[error("Validation failed: {reason}")]
ValidationFailed { reason: String },
#[error("Rendering failed: {reason}")]
RenderingFailed { reason: String },
#[error("Version incompatibility: expected {expected}, got {actual}")]
VersionIncompatible { expected: String, actual: String },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
#[derive(Error, Debug)]
pub enum BridgeError {
#[error("Invalid command: {command}")]
InvalidCommand { command: String },
#[error("Missing required argument: {arg}")]
MissingArgument { arg: String },
#[error("Execution failed: {reason}")]
ExecutionFailed { reason: String },
#[error("Provider error: {0}")]
Provider(#[from] ProviderError),
}
#[derive(Error, Debug)]
pub enum RenderError {
#[error("Syntax error at line {line}, column {column}: {message}")]
SyntaxError {
line: usize,
column: usize,
message: String,
},
#[error("Missing variable: {variable}")]
MissingVariable { variable: String },
#[error("Rendering failed: {reason}")]
RenderingFailed { reason: String },
}
#[cfg(test)]
pub fn verify_template_provider_contract<P: TemplateProvider>(
provider: P,
) -> Result<(), String> {
use std::path::{Path, PathBuf};
let test_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/templates");
let templates = provider
.discover(&test_path)
.map_err(|e| format!("discover failed: {}", e))?;
if templates.is_empty() {
return Err("Contract violation: discover returned empty list for valid path".to_string());
}
for template in &templates {
provider
.validate(template)
.map_err(|e| format!("validate failed for {}: {}", template.name, e))?;
}
if let Some(template) = templates.first() {
let context = Context::default();
provider
.render(template, context)
.map_err(|e| format!("render failed: {}", e))?;
}
if let Some(template) = templates.first() {
let _metadata = provider
.metadata(template)
.map_err(|e| format!("metadata failed: {}", e))?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_version_compatibility() {
let v1_0_0 = Version::new(1, 0, 0);
let v1_1_0 = Version::new(1, 1, 0);
let v2_0_0 = Version::new(2, 0, 0);
assert!(v1_1_0.is_compatible_with(&v1_0_0));
assert!(!v2_0_0.is_compatible_with(&v1_0_0));
assert!(!v1_0_0.is_compatible_with(&v1_1_0));
}
#[test]
fn test_version_display() {
let version = Version::new(1, 2, 3);
assert_eq!(version.to_string(), "1.2.3");
}
struct FilesystemTemplateProvider {
base_path: std::path::PathBuf,
}
impl TemplateProvider for FilesystemTemplateProvider {
fn discover(&self, path: &Path) -> Result<Vec<Template>, ProviderError> {
let mut templates = Vec::new();
if path.exists() && path.is_dir() {
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
if let Ok(metadata) = entry.metadata() {
if metadata.is_file() {
if let Ok(filename) = entry.file_name().into_string() {
if filename.ends_with(".tmpl") {
if let Ok(content) = std::fs::read_to_string(entry.path()) {
templates.push(Template {
name: filename[..filename.len() - 5].to_string(),
path: entry.path().to_string_lossy().to_string(),
content,
});
}
}
}
}
}
}
}
}
Ok(templates)
}
fn validate(&self, template: &Template) -> Result<(), ProviderError> {
if template.content.is_empty() {
return Err(ProviderError {
message: "Template content cannot be empty".to_string(),
});
}
if !template.content.contains("{{") {
return Err(ProviderError {
message: "Template must contain template variables".to_string(),
});
}
Ok(())
}
fn render(&self, template: &Template, _context: Context) -> Result<String, ProviderError> {
Ok(template.content.clone())
}
fn metadata(&self, template: &Template) -> Result<TemplateMetadata, ProviderError> {
Ok(TemplateMetadata {
name: template.name.clone(),
version: Version::new(1, 0, 0),
author: None,
description: None,
tags: vec![],
})
}
}
#[test]
fn test_filesystem_provider_satisfies_contract() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let template_path = temp_dir.path().join("test.tmpl");
fs::write(&template_path, "Hello {{ name }}!")
.expect("Failed to write template file");
let provider = FilesystemTemplateProvider {
base_path: temp_dir.path().to_path_buf(),
};
let templates = provider
.discover(temp_dir.path())
.expect("Discovery should succeed");
assert_eq!(templates.len(), 1, "Should discover one template");
assert_eq!(templates[0].name, "test");
assert!(templates[0].content.contains("{{ name }}"));
assert!(provider.validate(&templates[0]).is_ok(), "Valid template should pass");
let metadata = provider
.metadata(&templates[0])
.expect("Metadata should be readable");
assert_eq!(metadata.name, "test");
}
#[test]
fn test_template_validation_rejects_invalid_templates() {
use tempfile::TempDir;
use std::fs;
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let provider = FilesystemTemplateProvider {
base_path: temp_dir.path().to_path_buf(),
};
let empty_template = Template {
name: "empty".to_string(),
path: "empty.tmpl".to_string(),
content: "".to_string(),
};
assert!(
provider.validate(&empty_template).is_err(),
"Empty template should fail validation"
);
let no_vars_template = Template {
name: "no_vars".to_string(),
path: "no_vars.tmpl".to_string(),
content: "Just plain text".to_string(),
};
assert!(
provider.validate(&no_vars_template).is_err(),
"Template without variables should fail validation"
);
let valid_template = Template {
name: "valid".to_string(),
path: "valid.tmpl".to_string(),
content: "Hello {{ name }}!".to_string(),
};
assert!(
provider.validate(&valid_template).is_ok(),
"Valid template should pass validation"
);
}
}