use crate::utils::error::{Error, Result};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum NodeType {
Directory,
File,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileTreeNode {
#[serde(rename = "type")]
pub node_type: NodeType,
pub name: String,
#[serde(default)]
pub children: Vec<FileTreeNode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub content: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub template: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TemplateFormat {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub rdf: BTreeMap<String, serde_yaml::Value>,
#[serde(default)]
pub variables: Vec<String>,
#[serde(default)]
pub defaults: BTreeMap<String, String>,
pub tree: Vec<FileTreeNode>,
}
impl TemplateFormat {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
rdf: BTreeMap::new(),
variables: Vec::new(),
defaults: BTreeMap::new(),
tree: Vec::new(),
}
}
pub fn add_variable(&mut self, var: impl Into<String>) -> &mut Self {
self.variables.push(var.into());
self
}
pub fn add_default(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
self.defaults.insert(key.into(), value.into());
self
}
pub fn add_node(&mut self, node: FileTreeNode) -> &mut Self {
self.tree.push(node);
self
}
pub fn from_yaml(yaml: &str) -> Result<Self> {
serde_yaml::from_str(yaml).map_err(|e| {
Error::with_context("Failed to parse template format from YAML", &e.to_string())
})
}
pub fn to_yaml(&self) -> Result<String> {
serde_yaml::to_string(self).map_err(|e| {
Error::with_context(
"Failed to serialize template format to YAML",
&e.to_string(),
)
})
}
pub fn validate(&self) -> Result<()> {
if self.name.is_empty() {
return Err(crate::utils::error::Error::new(
"Template name cannot be empty",
));
}
if self.tree.is_empty() {
return Err(crate::utils::error::Error::new(
"Template must contain at least one tree node",
));
}
Self::validate_nodes(&self.tree)?;
Ok(())
}
fn validate_nodes(nodes: &[FileTreeNode]) -> Result<()> {
for node in nodes {
if node.name.is_empty() {
return Err(crate::utils::error::Error::new("Node name cannot be empty"));
}
match node.node_type {
NodeType::File => {
if node.content.is_none() && node.template.is_none() {
return Err(crate::utils::error::Error::new(&format!(
"File node '{}' must have either content or template",
node.name
)));
}
if !node.children.is_empty() {
return Err(crate::utils::error::Error::new(&format!(
"File node '{}' cannot have children",
node.name
)));
}
}
NodeType::Directory => {
if node.content.is_some() || node.template.is_some() {
return Err(crate::utils::error::Error::new(&format!(
"Directory node '{}' cannot have content or template",
node.name
)));
}
Self::validate_nodes(&node.children)?;
}
}
}
Ok(())
}
}
impl FileTreeNode {
pub fn directory(name: impl Into<String>) -> Self {
Self {
node_type: NodeType::Directory,
name: name.into(),
children: Vec::new(),
content: None,
template: None,
mode: None,
}
}
pub fn file_with_content(name: impl Into<String>, content: impl Into<String>) -> Self {
Self {
node_type: NodeType::File,
name: name.into(),
children: Vec::new(),
content: Some(content.into()),
template: None,
mode: None,
}
}
pub fn file_with_template(name: impl Into<String>, template: impl Into<String>) -> Self {
Self {
node_type: NodeType::File,
name: name.into(),
children: Vec::new(),
content: None,
template: Some(template.into()),
mode: None,
}
}
pub fn add_child(&mut self, child: FileTreeNode) -> &mut Self {
self.children.push(child);
self
}
pub fn with_mode(mut self, mode: u32) -> Self {
self.mode = Some(mode);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_directory_node() {
let node = FileTreeNode::directory("src");
assert_eq!(node.node_type, NodeType::Directory);
assert_eq!(node.name, "src");
assert!(node.children.is_empty());
}
#[test]
fn test_file_node_with_content() {
let node = FileTreeNode::file_with_content("main.rs", "fn main() {}");
assert_eq!(node.node_type, NodeType::File);
assert_eq!(node.name, "main.rs");
assert_eq!(node.content, Some("fn main() {}".to_string()));
assert_eq!(node.template, None);
}
#[test]
fn test_file_node_with_template() {
let node = FileTreeNode::file_with_template("lib.rs", "templates/lib.rs.tera");
assert_eq!(node.node_type, NodeType::File);
assert_eq!(node.name, "lib.rs");
assert_eq!(node.template, Some("templates/lib.rs.tera".to_string()));
assert_eq!(node.content, None);
}
#[test]
fn test_template_format_creation() {
let mut format = TemplateFormat::new("test-template");
format.add_variable("service_name");
format.add_default("port", "8080");
assert_eq!(format.name, "test-template");
assert_eq!(format.variables, vec!["service_name"]);
assert_eq!(format.defaults.get("port"), Some(&"8080".to_string()));
}
#[test]
fn test_template_format_validation() {
let mut format = TemplateFormat::new("test");
format.add_node(FileTreeNode::directory("src"));
assert!(format.validate().is_ok());
}
#[test]
fn test_empty_template_validation_fails() {
let format = TemplateFormat::new("test");
assert!(format.validate().is_err());
}
}