#![allow(dead_code)]
use anyhow::{anyhow, Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateManifest {
pub id: String,
pub version: String,
pub display_name: String,
pub description: String,
#[serde(default)]
pub files: FileSpec,
#[serde(default)]
pub placeholders: Vec<Placeholder>,
#[serde(default)]
pub post_generate: Vec<PostGenerateScript>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FileSpec {
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Placeholder {
pub name: String,
pub r#type: PlaceholderType,
pub description: String,
#[serde(default)]
pub required: bool,
#[serde(default)]
pub default: Option<String>,
#[serde(default)]
pub choices: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum PlaceholderType {
String,
Boolean,
Enum,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PostGenerateScript {
pub name: String,
pub description: String,
pub command: String,
#[serde(default)]
pub optional: bool,
}
impl TemplateManifest {
pub fn from_file(path: &Path) -> Result<Self> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("Failed to read template.toml: {}", path.display()))?;
Self::from_str(&content)
}
pub fn from_str(content: &str) -> Result<Self> {
let manifest: TemplateManifest = toml::from_str(content)
.context("Failed to parse template.toml — invalid TOML syntax")?;
if manifest.id.is_empty() {
return Err(anyhow!("Template manifest: id is required"));
}
if manifest.version.is_empty() {
return Err(anyhow!("Template manifest: version is required"));
}
if manifest.display_name.is_empty() {
return Err(anyhow!("Template manifest: display_name is required"));
}
let mut placeholder_names = std::collections::HashSet::new();
for ph in &manifest.placeholders {
if ph.name.is_empty() {
return Err(anyhow!(
"Template manifest: placeholder name cannot be empty"
));
}
if !placeholder_names.insert(&ph.name) {
return Err(anyhow!(
"Template manifest: duplicate placeholder name '{}'",
ph.name
));
}
if ph.r#type == PlaceholderType::Enum && ph.choices.is_none() {
return Err(anyhow!(
"Template manifest: enum placeholder '{}' must have 'choices'",
ph.name
));
}
}
Ok(manifest)
}
pub fn get_placeholder(&self, name: &str) -> Option<&Placeholder> {
self.placeholders.iter().find(|p| p.name == name)
}
pub fn required_placeholders(&self) -> Vec<&Placeholder> {
self.placeholders.iter().filter(|p| p.required).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_manifest() {
let toml = r#"
id = "basic-article"
version = "1.0.0"
display_name = "Basic Article"
description = "A simple article template"
[[placeholders]]
name = "title"
type = "string"
description = "Document title"
required = true
"#;
let manifest = TemplateManifest::from_str(toml).unwrap();
assert_eq!(manifest.id, "basic-article");
assert_eq!(manifest.placeholders.len(), 1);
assert!(manifest.get_placeholder("title").is_some());
}
#[test]
fn test_parse_with_default() {
let toml = r#"
id = "test"
version = "1.0.0"
display_name = "Test"
description = "Test template"
[[placeholders]]
name = "author"
type = "string"
description = "Author name"
default = "{{user.name}}"
"#;
let manifest = TemplateManifest::from_str(toml).unwrap();
let author = manifest.get_placeholder("author").unwrap();
assert_eq!(author.default, Some("{{user.name}}".to_string()));
}
#[test]
fn test_enum_placeholder_requires_choices() {
let toml = r#"
id = "test"
version = "1.0.0"
display_name = "Test"
description = "Test"
[[placeholders]]
name = "language"
type = "enum"
description = "Document language"
"#;
let result = TemplateManifest::from_str(toml);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("must have 'choices'"));
}
#[test]
fn test_duplicate_placeholder_names() {
let toml = r#"
id = "test"
version = "1.0.0"
display_name = "Test"
description = "Test"
[[placeholders]]
name = "title"
type = "string"
description = "Title"
[[placeholders]]
name = "title"
type = "string"
description = "Duplicate"
"#;
let result = TemplateManifest::from_str(toml);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("duplicate"));
}
}