use crate::models::error::TemplateError;
use crate::models::template::{GeneratedTemplate, TemplateResource};
use crate::services::context::{analyze_project, format_context_as_markdown};
use crate::services::renderer;
use crate::TemplateServerTrait;
use serde_json::Map;
use sha2::{Digest, Sha256};
use std::path::Path;
use std::sync::Arc;
pub async fn get_template_content<T: TemplateServerTrait>(
server: &T,
uri: &str,
) -> Result<String, TemplateError> {
server
.get_template_content(uri)
.await
.map(|arc_str| arc_str.to_string())
.map_err(|_| TemplateError::NotFound(format!("Template content not found: {uri}")))
}
pub async fn generate_template<T: TemplateServerTrait>(
server: &T,
uri: &str,
parameters: Map<String, serde_json::Value>,
) -> Result<GeneratedTemplate, TemplateError> {
let (category, toolchain, _variant) = parse_template_uri(uri)?;
if category == "context" {
return generate_context(toolchain, parameters).await;
}
let metadata =
server
.get_template_metadata(uri)
.await
.map_err(|_| TemplateError::TemplateNotFound {
uri: uri.to_string(),
})?;
validate_parameters(&metadata.parameters, ¶meters)?;
let template_content = server
.get_template_content(uri)
.await
.map_err(|_| TemplateError::NotFound(format!("Template content not found: {uri}")))?;
let rendered =
renderer::render_template(server.get_renderer(), &template_content, parameters.clone())?;
let mut hasher = Sha256::new();
hasher.update(rendered.as_bytes());
let checksum = hex::encode(hasher.finalize());
let project_name = parameters
.get("project_name")
.and_then(|v| v.as_str())
.unwrap_or("project");
Ok(GeneratedTemplate {
content: rendered,
filename: format!("{}/{}", project_name, extract_filename(category)),
checksum,
toolchain: metadata.toolchain.clone(),
})
}
async fn generate_context(
toolchain: &str,
parameters: Map<String, serde_json::Value>,
) -> Result<GeneratedTemplate, TemplateError> {
let project_path = parameters
.get("project_path")
.and_then(|v| v.as_str())
.unwrap_or(".");
let context = analyze_project(Path::new(project_path), toolchain).await?;
let content = format_context_as_markdown(&context);
let mut hasher = Sha256::new();
hasher.update(content.as_bytes());
let checksum = hex::encode(hasher.finalize());
let toolchain_enum = match toolchain {
"rust" => crate::models::template::Toolchain::RustCli {
cargo_features: vec![],
},
"deno" => crate::models::template::Toolchain::DenoTypescript {
deno_version: "1.46".to_string(),
},
"python-uv" => crate::models::template::Toolchain::PythonUv {
python_version: "3.12".to_string(),
},
_ => {
return Err(TemplateError::InvalidUri {
uri: format!("template://context/{toolchain}/ast"),
})
}
};
Ok(GeneratedTemplate {
content,
filename: "CONTEXT.md".to_string(),
checksum,
toolchain: toolchain_enum,
})
}
pub async fn list_templates<T: TemplateServerTrait>(
server: &T,
toolchain: Option<&str>,
category: Option<&str>,
) -> Result<Vec<Arc<TemplateResource>>, TemplateError> {
let prefix = build_template_prefix(category, toolchain);
let mut templates = server.list_templates(&prefix).await.map_err(|_| {
TemplateError::NotFound(format!("Failed to list templates with prefix: {prefix}"))
})?;
if let Some(tc) = toolchain {
if category.is_none() {
templates.retain(|t| t.toolchain.as_str() == tc);
}
}
templates.sort_by(|a, b| {
a.toolchain
.priority()
.cmp(&b.toolchain.priority())
.then_with(|| b.semantic_version.cmp(&a.semantic_version))
});
Ok(templates)
}
pub async fn list_all_resources<T: TemplateServerTrait>(
server: &T,
) -> Result<Vec<Arc<TemplateResource>>, TemplateError> {
list_templates(server, None, None).await
}
fn parse_template_uri(uri: &str) -> Result<(&str, &str, &str), TemplateError> {
let parts: Vec<&str> = uri
.strip_prefix("template://")
.ok_or_else(|| TemplateError::InvalidUri {
uri: uri.to_string(),
})?
.split('/')
.collect();
if parts.len() != 3 {
return Err(TemplateError::InvalidUri {
uri: uri.to_string(),
});
}
Ok((parts[0], parts[1], parts[2]))
}
fn build_template_prefix(category: Option<&str>, toolchain: Option<&str>) -> String {
match (category, toolchain) {
(None, None) => String::new(), (Some(cat), None) => format!("{cat}/"),
(Some(cat), Some(tc)) => format!("{cat}/{tc}/"),
(None, Some(_)) => String::new(), }
}
fn extract_filename(category: &str) -> String {
match category {
"makefile" => "Makefile".to_string(),
"readme" => "README.md".to_string(),
"gitignore" => ".gitignore".to_string(),
_ => format!("{category}.txt"),
}
}
fn validate_parameters(
specs: &[crate::models::template::ParameterSpec],
provided: &Map<String, serde_json::Value>,
) -> Result<(), TemplateError> {
for spec in specs {
if spec.required && !provided.contains_key(&spec.name) {
return Err(TemplateError::ValidationError {
parameter: spec.name.clone(),
reason: "required parameter missing".to_string(),
});
}
if let Some(value) = provided.get(&spec.name) {
if let Some(pattern) = &spec.validation_pattern {
let regex =
regex::Regex::new(pattern).map_err(|_| TemplateError::ValidationError {
parameter: spec.name.clone(),
reason: "invalid validation pattern".to_string(),
})?;
if let Some(str_value) = value.as_str() {
if !regex.is_match(str_value) {
return Err(TemplateError::ValidationError {
parameter: spec.name.clone(),
reason: format!("value does not match pattern: {pattern}"),
});
}
}
}
}
}
Ok(())
}
pub async fn scaffold_project<T: TemplateServerTrait>(
server: Arc<T>,
toolchain: &str,
templates: Vec<String>,
parameters: serde_json::Value,
) -> Result<ScaffoldResult, TemplateError> {
let mut files = Vec::new();
let mut errors = Vec::new();
let params_map = if let serde_json::Value::Object(map) = parameters {
map
} else {
return Err(TemplateError::ValidationError {
parameter: "parameters".to_string(),
reason: "Parameters must be an object".to_string(),
});
};
for template_type in &templates {
let variant = match template_type.as_str() {
"makefile" | "readme" | "gitignore" => match toolchain {
"rust" | "deno" | "python-uv" => "cli",
_ => continue,
},
_ => continue,
};
let uri = format!("template://{template_type}/{toolchain}/{variant}");
match generate_template(server.as_ref(), &uri, params_map.clone()).await {
Ok(generated) => {
files.push(GeneratedFile {
path: generated.filename,
content: generated.content,
checksum: generated.checksum,
});
}
Err(e) => {
errors.push(ScaffoldError {
template: template_type.clone(),
error: e.to_string(),
});
}
}
}
Ok(ScaffoldResult { files, errors })
}
pub async fn search_templates<T: TemplateServerTrait>(
server: Arc<T>,
query: &str,
toolchain: Option<&str>,
) -> Result<Vec<SearchResult>, TemplateError> {
let templates = list_templates(server.as_ref(), toolchain, None).await?;
let query_lower = query.to_lowercase();
let mut results: Vec<SearchResult> = templates
.into_iter()
.filter_map(|template| {
let mut matches = Vec::new();
let mut relevance = 0.0;
if template.name.to_lowercase().contains(&query_lower) {
matches.push(format!("name: {}", template.name));
relevance += if template.name.to_lowercase() == query_lower {
10.0
} else {
5.0
};
}
if template.description.to_lowercase().contains(&query_lower) {
matches.push("description".to_string());
relevance += 3.0;
}
for param in &template.parameters {
if param.name.to_lowercase().contains(&query_lower) {
matches.push(format!("parameter: {}", param.name));
relevance += 1.0;
}
}
if matches.is_empty() {
None
} else {
Some(SearchResult {
template: (*template).clone(),
relevance,
matches,
})
}
})
.collect();
results.sort_by(|a, b| b.relevance.partial_cmp(&a.relevance).unwrap());
Ok(results)
}
pub async fn validate_template<T: TemplateServerTrait>(
server: Arc<T>,
uri: &str,
parameters: &serde_json::Value,
) -> Result<ValidationResult, TemplateError> {
let metadata = get_template_metadata(server, uri).await?;
let params_map = match extract_params_map(parameters) {
Ok(map) => map,
Err(validation_result) => return Ok(validation_result),
};
let mut errors = Vec::new();
validate_required_parameters(&metadata.parameters, params_map, &mut errors);
validate_parameter_values(&metadata.parameters, params_map, &mut errors);
Ok(ValidationResult {
valid: errors.is_empty(),
errors,
})
}
async fn get_template_metadata<T: TemplateServerTrait>(
server: Arc<T>,
uri: &str,
) -> Result<crate::models::template::TemplateResource, TemplateError> {
server
.get_template_metadata(uri)
.await
.map(|arc_resource| (*arc_resource).clone())
.map_err(|_| TemplateError::TemplateNotFound {
uri: uri.to_string(),
})
}
fn extract_params_map(
parameters: &serde_json::Value,
) -> Result<&Map<String, serde_json::Value>, ValidationResult> {
if let serde_json::Value::Object(map) = parameters { Ok(map) } else {
let error = ValidationError {
field: "parameters".to_string(),
message: "Parameters must be an object".to_string(),
};
Err(ValidationResult {
valid: false,
errors: vec![error],
})
}
}
fn validate_required_parameters(
param_specs: &[crate::models::template::ParameterSpec],
params_map: &Map<String, serde_json::Value>,
errors: &mut Vec<ValidationError>,
) {
for param in param_specs {
if param.required && !params_map.contains_key(¶m.name) {
errors.push(ValidationError {
field: param.name.clone(),
message: "Required parameter missing".to_string(),
});
}
}
}
fn validate_parameter_values(
param_specs: &[crate::models::template::ParameterSpec],
params_map: &Map<String, serde_json::Value>,
errors: &mut Vec<ValidationError>,
) {
for (key, value) in params_map {
if let Some(param_spec) = param_specs.iter().find(|p| p.name == *key) {
validate_parameter_pattern(param_spec, key, value, errors);
} else {
errors.push(ValidationError {
field: key.clone(),
message: "Unknown parameter".to_string(),
});
}
}
}
fn validate_parameter_pattern(
param_spec: &crate::models::template::ParameterSpec,
key: &str,
value: &serde_json::Value,
errors: &mut Vec<ValidationError>,
) {
if let Some(pattern) = ¶m_spec.validation_pattern {
if let Ok(regex) = regex::Regex::new(pattern) {
if let Some(str_val) = value.as_str() {
if !regex.is_match(str_val) {
errors.push(ValidationError {
field: key.to_string(),
message: format!("Does not match pattern: {pattern}"),
});
}
}
}
}
}
#[derive(Debug)]
pub struct ScaffoldResult {
pub files: Vec<GeneratedFile>,
pub errors: Vec<ScaffoldError>,
}
#[derive(Debug)]
pub struct GeneratedFile {
pub path: String,
pub content: String,
pub checksum: String,
}
#[derive(Debug)]
pub struct ScaffoldError {
pub template: String,
pub error: String,
}
#[derive(Debug)]
pub struct SearchResult {
pub template: crate::models::template::TemplateResource,
pub relevance: f32,
pub matches: Vec<String>,
}
#[derive(Debug)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
}
#[derive(Debug)]
pub struct ValidationError {
pub field: String,
pub message: String,
}
#[cfg(test)]
mod tests {
#[test]
fn test_template_service_basic() {
assert_eq!(1 + 1, 2);
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}