use ggen_utils::error::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
#[derive(Debug, Clone)]
pub struct TemplateMetadata {
pub name: String,
pub path: String,
pub description: Option<String>,
pub variables: Vec<String>,
pub output_path: Option<String>,
pub rdf_sources: Vec<String>,
pub sparql_queries: HashMap<String, String>,
pub determinism_seed: Option<u64>,
}
pub fn show_template_metadata(template_ref: &str) -> Result<TemplateMetadata> {
let template_path = if template_ref.starts_with("gpack:") {
return Err(ggen_utils::error::Error::new(
"gpack templates not yet supported",
));
} else if template_ref.contains('/') {
template_ref.to_string()
} else {
format!("templates/{}", template_ref)
};
let path = std::path::Path::new(&template_path);
if !path.exists() {
return Err(ggen_utils::error::Error::new(&format!(
"Template not found: {}",
template_path
)));
}
let content = fs::read_to_string(&template_path)
.map_err(|e| ggen_utils::error::Error::new(&format!("Failed to read template: {}", e)))?;
parse_template_metadata(&content, &template_path)
}
fn parse_template_metadata(content: &str, path: &str) -> Result<TemplateMetadata> {
let name = std::path::Path::new(path)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
if content.starts_with("---\n") {
if let Some(end_pos) = content.find("\n---\n") {
let frontmatter = &content[4..end_pos];
return parse_yaml_frontmatter(frontmatter, &name, path);
}
}
Ok(TemplateMetadata {
name,
path: path.to_string(),
description: None,
variables: extract_variables_from_content(content),
output_path: None,
rdf_sources: vec![],
sparql_queries: HashMap::new(),
determinism_seed: None,
})
}
fn parse_yaml_frontmatter(frontmatter: &str, name: &str, path: &str) -> Result<TemplateMetadata> {
let mut metadata = TemplateMetadata {
name: name.to_string(),
path: path.to_string(),
description: None,
variables: vec![],
output_path: None,
rdf_sources: vec![],
sparql_queries: HashMap::new(),
determinism_seed: None,
};
for line in frontmatter.lines() {
let line = line.trim();
if let Some(stripped) = line.strip_prefix("to:") {
metadata.output_path = Some(stripped.trim().to_string());
} else if line.starts_with("vars:")
|| line.starts_with("rdf:")
|| line.starts_with("sparql:")
|| line.starts_with("determinism:")
{
continue;
} else if let Some(stripped) = line.strip_prefix("- ") {
let var = stripped.trim();
if !var.is_empty() {
metadata.variables.push(var.to_string());
}
} else if line.contains(':') && !line.starts_with(" ") {
if let Some((key, value)) = line.split_once(':') {
let key = key.trim();
let value = value.trim();
if key == "seed" {
if let Ok(seed) = value.parse::<u64>() {
metadata.determinism_seed = Some(seed);
}
}
}
}
}
Ok(metadata)
}
fn extract_variables_from_content(content: &str) -> Vec<String> {
let mut variables = Vec::new();
let re = regex::Regex::new(r"\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}").unwrap();
for cap in re.captures_iter(content) {
if let Some(var) = cap.get(1) {
let var_name = var.as_str().to_string();
if !variables.contains(&var_name) {
variables.push(var_name);
}
}
}
variables
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_variables() {
let content = "Hello {{ name }}, your age is {{ age }}. {{ name }} again.";
let vars = extract_variables_from_content(content);
assert_eq!(vars.len(), 2);
assert!(vars.contains(&"name".to_string()));
assert!(vars.contains(&"age".to_string()));
}
#[test]
fn test_parse_yaml_frontmatter() {
let frontmatter = r#"to: output.txt
vars:
- name
- age
seed: 42"#;
let metadata = parse_yaml_frontmatter(frontmatter, "test", "test.tmpl").unwrap();
assert_eq!(metadata.name, "test");
assert_eq!(metadata.output_path, Some("output.txt".to_string()));
assert_eq!(metadata.variables.len(), 2);
assert_eq!(metadata.determinism_seed, Some(42));
}
#[test]
fn test_parse_metadata_with_frontmatter() {
let content = r#"---
to: {{ name }}.txt
vars:
- name
---
Hello {{ name }}!"#;
let metadata = parse_template_metadata(content, "test.tmpl").unwrap();
assert_eq!(metadata.name, "test.tmpl");
assert_eq!(metadata.output_path, Some("{{ name }}.txt".to_string()));
}
#[test]
fn test_parse_metadata_without_frontmatter() {
let content = "Hello {{ name }}!";
let metadata = parse_template_metadata(content, "test.tmpl").unwrap();
assert_eq!(metadata.name, "test.tmpl");
assert_eq!(metadata.variables.len(), 1);
assert!(metadata.variables.contains(&"name".to_string()));
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ShowInput {
pub template: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ShowOutput {
pub name: String,
pub path: String,
pub description: Option<String>,
pub variables: Vec<String>,
pub output_path: Option<String>,
pub rdf_sources: Vec<String>,
pub sparql_queries: HashMap<String, String>,
pub determinism_seed: Option<u64>,
}
impl From<TemplateMetadata> for ShowOutput {
fn from(metadata: TemplateMetadata) -> Self {
Self {
name: metadata.name,
path: metadata.path,
description: metadata.description,
variables: metadata.variables,
output_path: metadata.output_path,
rdf_sources: metadata.rdf_sources,
sparql_queries: metadata.sparql_queries,
determinism_seed: metadata.determinism_seed,
}
}
}
pub fn execute_show(input: ShowInput) -> Result<ShowOutput> {
let metadata = show_template_metadata(&input.template)?;
Ok(ShowOutput::from(metadata))
}
pub fn run(args: &ShowInput) -> Result<()> {
let output = execute_show(args.clone())?;
ggen_utils::alert_info!("📋 Template: {}", output.name);
ggen_utils::alert_info!("📍 Path: {}", output.path);
if let Some(desc) = &output.description {
ggen_utils::alert_info!("📝 Description: {}", desc);
}
if let Some(output_path) = &output.output_path {
ggen_utils::alert_info!("📂 Output path: {}", output_path);
}
if !output.variables.is_empty() {
ggen_utils::alert_info!("\n🔧 Variables ({}):", output.variables.len());
for var in &output.variables {
ggen_utils::alert_info!(" • {}", var);
}
}
if !output.rdf_sources.is_empty() {
ggen_utils::alert_info!("\n🔗 RDF Sources ({}):", output.rdf_sources.len());
for source in &output.rdf_sources {
ggen_utils::alert_info!(" • {}", source);
}
}
if !output.sparql_queries.is_empty() {
ggen_utils::alert_info!("\n🔍 SPARQL Queries ({}):", output.sparql_queries.len());
for (name, query) in &output.sparql_queries {
ggen_utils::alert_info!(" • {}: {}", name, query);
}
}
if let Some(seed) = output.determinism_seed {
ggen_utils::alert_info!("\n🎲 Determinism seed: {}", seed);
}
Ok(())
}