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::PathBuf;
let test_path = PathBuf::from("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 MockTemplateProvider;
impl TemplateProvider for MockTemplateProvider {
fn discover(&self, _path: &Path) -> Result<Vec<Template>, ProviderError> {
Ok(vec![Template {
name: "test".to_string(),
path: "test.tmpl".to_string(),
content: "{{ name }}".to_string(),
}])
}
fn validate(&self, _template: &Template) -> Result<(), ProviderError> {
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_mock_provider_satisfies_contract() {
let provider = MockTemplateProvider;
verify_template_provider_contract(provider).unwrap();
}
}