Skip to main content

ggen_core/templates/
format.rs

1//! Template format definitions
2//!
3//! Defines the structure and parsing of file tree templates.
4//!
5//! ## Features
6//!
7//! - **File tree representation**: Hierarchical structure for directory and file nodes
8//! - **Template format parsing**: Parse YAML/JSON template definitions
9//! - **Node types**: Support for directories and files
10//! - **Metadata support**: Attach metadata to nodes
11//!
12//! ## Examples
13//!
14//! ### Creating a File Tree Template
15//!
16//! ```rust,no_run
17//! use crate::templates::format::{FileTreeNode, TemplateFormat};
18//!
19//! # fn main() -> crate::utils::error::Result<()> {
20//! let mut template = TemplateFormat::new("my-template");
21//!
22//! let mut src = FileTreeNode::directory("src");
23//! src.children.push(FileTreeNode::file_with_content(
24//!     "main.rs",
25//!     "fn main() { println!(\"Hello\"); }",
26//! ));
27//! template.add_node(src);
28//!
29//! assert_eq!(template.name, "my-template");
30//! # Ok(())
31//! # }
32//! ```
33//!
34//! ### Parsing Template Format
35//!
36//! ```rust,no_run
37//! use crate::templates::format::TemplateFormat;
38//!
39//! # fn main() -> crate::utils::error::Result<()> {
40//! let yaml = r#"
41//! nodes:
42//!   - name: src
43//!     type: directory
44//!     children:
45//!       - name: main.rs
46//!         type: file
47//!         content: "fn main() {}"
48//! "#;
49//!
50//! let template: TemplateFormat = serde_yaml::from_str(yaml)?;
51//! # Ok(())
52//! # }
53//! ```
54
55use crate::utils::error::{Error, Result};
56use serde::{Deserialize, Serialize};
57use std::collections::BTreeMap;
58
59/// Node type in the file tree template
60///
61/// Represents whether a node in the file tree is a directory or a file.
62///
63/// # Examples
64///
65/// ```rust
66/// use crate::templates::format::NodeType;
67///
68/// let dir_type = NodeType::Directory;
69/// let file_type = NodeType::File;
70///
71/// assert_ne!(dir_type, file_type);
72/// ```
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
74#[serde(rename_all = "lowercase")]
75pub enum NodeType {
76    /// Directory node - can contain child nodes
77    Directory,
78    /// File node - contains content or references a template
79    File,
80}
81
82/// A node in the file tree template
83///
84/// Represents either a file or directory in the generated file tree.
85/// Directory nodes can contain children, while file nodes contain content
86/// or reference template files.
87///
88/// # Examples
89///
90/// ## Creating a directory node
91///
92/// ```rust
93/// use crate::templates::format::FileTreeNode;
94///
95/// let dir = FileTreeNode::directory("src");
96/// assert_eq!(dir.name, "src");
97/// ```
98///
99/// ## Creating a file node with content
100///
101/// ```rust
102/// use crate::templates::format::FileTreeNode;
103///
104/// let file = FileTreeNode::file_with_content("main.rs", "fn main() {}");
105/// assert_eq!(file.name, "main.rs");
106/// assert_eq!(file.content, Some("fn main() {}".to_string()));
107/// ```
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct FileTreeNode {
110    /// Type of node (file or directory)
111    #[serde(rename = "type")]
112    pub node_type: NodeType,
113
114    /// Name of the file or directory (may contain template variables)
115    pub name: String,
116
117    /// Children nodes (for directories)
118    #[serde(default)]
119    pub children: Vec<FileTreeNode>,
120
121    /// Inline content (for files)
122    #[serde(skip_serializing_if = "Option::is_none")]
123    pub content: Option<String>,
124
125    /// Template file reference (for files)
126    #[serde(skip_serializing_if = "Option::is_none")]
127    pub template: Option<String>,
128
129    /// File permissions (Unix mode)
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub mode: Option<u32>,
132}
133
134/// Template format with metadata and RDF support
135///
136/// Represents a complete file tree template with metadata, variables, defaults,
137/// and RDF annotations. This is the top-level structure for file tree templates.
138///
139/// # Examples
140///
141/// ```rust
142/// use crate::templates::format::{TemplateFormat, FileTreeNode};
143///
144/// let mut format = TemplateFormat::new("my-template");
145/// format.add_variable("service_name");
146/// format.add_default("port", "8080");
147/// format.add_node(FileTreeNode::directory("src"));
148///
149/// assert_eq!(format.name, "my-template");
150/// ```
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct TemplateFormat {
153    /// Template name
154    pub name: String,
155
156    /// Template description
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub description: Option<String>,
159
160    /// RDF metadata
161    #[serde(default)]
162    pub rdf: BTreeMap<String, serde_yaml::Value>,
163
164    /// Required variables
165    #[serde(default)]
166    pub variables: Vec<String>,
167
168    /// Default variable values
169    #[serde(default)]
170    pub defaults: BTreeMap<String, String>,
171
172    /// Root nodes of the file tree
173    pub tree: Vec<FileTreeNode>,
174}
175
176impl TemplateFormat {
177    /// Create a new template format
178    ///
179    /// Creates an empty template format with the given name. Variables, defaults,
180    /// and nodes can be added using builder methods.
181    ///
182    /// # Arguments
183    ///
184    /// * `name` - Template name (must be non-empty)
185    ///
186    /// # Returns
187    ///
188    /// A new `TemplateFormat` with empty variables, defaults, and tree.
189    ///
190    /// # Examples
191    ///
192    /// ```rust
193    /// use crate::templates::format::TemplateFormat;
194    ///
195    /// let format = TemplateFormat::new("my-template");
196    /// assert_eq!(format.name, "my-template");
197    /// assert!(format.variables.is_empty());
198    /// assert!(format.tree.is_empty());
199    /// ```
200    pub fn new(name: impl Into<String>) -> Self {
201        Self {
202            name: name.into(),
203            description: None,
204            rdf: BTreeMap::new(),
205            variables: Vec::new(),
206            defaults: BTreeMap::new(),
207            tree: Vec::new(),
208        }
209    }
210
211    /// Add a variable to the template
212    ///
213    /// Adds a required variable to the template. Variables must be provided
214    /// when generating from this template (unless they have defaults).
215    /// Returns `&mut Self` for method chaining.
216    ///
217    /// # Arguments
218    ///
219    /// * `var` - Variable name to add
220    ///
221    /// # Returns
222    ///
223    /// `&mut Self` for method chaining.
224    ///
225    /// # Examples
226    ///
227    /// ```rust
228    /// use crate::templates::format::TemplateFormat;
229    ///
230    /// let mut format = TemplateFormat::new("my-template");
231    /// format.add_variable("service_name")
232    ///       .add_variable("port");
233    ///
234    /// assert_eq!(format.variables.len(), 2);
235    /// assert!(format.variables.contains(&"service_name".to_string()));
236    /// ```
237    pub fn add_variable(&mut self, var: impl Into<String>) -> &mut Self {
238        self.variables.push(var.into());
239        self
240    }
241
242    /// Add a default value for a variable
243    ///
244    /// Sets a default value for a variable. Defaults are only applied if the
245    /// variable is not provided during generation. Returns `&mut Self` for method chaining.
246    ///
247    /// # Arguments
248    ///
249    /// * `key` - Variable name
250    /// * `value` - Default value (as string)
251    ///
252    /// # Returns
253    ///
254    /// `&mut Self` for method chaining.
255    ///
256    /// # Examples
257    ///
258    /// ```rust
259    /// use crate::templates::format::TemplateFormat;
260    ///
261    /// let mut format = TemplateFormat::new("my-template");
262    /// format.add_variable("port")
263    ///       .add_default("port", "8080");
264    ///
265    /// assert_eq!(format.defaults.get("port"), Some(&"8080".to_string()));
266    /// ```
267    pub fn add_default(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
268        self.defaults.insert(key.into(), value.into());
269        self
270    }
271
272    /// Add a tree node
273    ///
274    /// Adds a root-level node to the file tree. Returns `&mut Self` for method chaining.
275    ///
276    /// # Arguments
277    ///
278    /// * `node` - File tree node to add
279    ///
280    /// # Returns
281    ///
282    /// `&mut Self` for method chaining.
283    ///
284    /// # Examples
285    ///
286    /// ```rust
287    /// use crate::templates::format::{TemplateFormat, FileTreeNode};
288    ///
289    /// let mut format = TemplateFormat::new("my-template");
290    /// format.add_node(FileTreeNode::directory("src"))
291    ///       .add_node(FileTreeNode::directory("tests"));
292    ///
293    /// assert_eq!(format.tree.len(), 2);
294    /// ```
295    pub fn add_node(&mut self, node: FileTreeNode) -> &mut Self {
296        self.tree.push(node);
297        self
298    }
299
300    /// Parse from YAML string
301    ///
302    /// Parses a template format from a YAML string. The YAML should match
303    /// the `TemplateFormat` structure.
304    ///
305    /// # Arguments
306    ///
307    /// * `yaml` - YAML string to parse
308    ///
309    /// # Returns
310    ///
311    /// A parsed `TemplateFormat` on success.
312    ///
313    /// # Errors
314    ///
315    /// Returns an error if the YAML is invalid or doesn't match the expected structure.
316    ///
317    /// # Examples
318    ///
319    /// ```rust
320    /// use crate::templates::format::TemplateFormat;
321    ///
322    /// # fn main() -> crate::utils::error::Result<()> {
323    /// let yaml = r#"
324    /// name: my-template
325    /// variables:
326    ///   - service_name
327    /// tree:
328    ///   - name: src
329    ///     type: directory
330    /// "#;
331    ///
332    /// let format = TemplateFormat::from_yaml(yaml)?;
333    /// assert_eq!(format.name, "my-template");
334    /// # Ok(())
335    /// # }
336    /// ```
337    pub fn from_yaml(yaml: &str) -> Result<Self> {
338        serde_yaml::from_str(yaml).map_err(|e| {
339            Error::with_context("Failed to parse template format from YAML", &e.to_string())
340        })
341    }
342
343    /// Serialize to YAML string
344    ///
345    /// Converts this template format to a YAML string representation.
346    ///
347    /// # Returns
348    ///
349    /// YAML string representation of the template format.
350    ///
351    /// # Errors
352    ///
353    /// Returns an error if serialization fails.
354    ///
355    /// # Examples
356    ///
357    /// ```rust
358    /// use crate::templates::format::{TemplateFormat, FileTreeNode};
359    ///
360    /// # fn main() -> crate::utils::error::Result<()> {
361    /// let mut format = TemplateFormat::new("my-template");
362    /// format.add_node(FileTreeNode::directory("src"));
363    ///
364    /// let yaml = format.to_yaml()?;
365    /// assert!(yaml.contains("name: my-template"));
366    /// # Ok(())
367    /// # }
368    /// ```
369    pub fn to_yaml(&self) -> Result<String> {
370        serde_yaml::to_string(self).map_err(|e| {
371            Error::with_context(
372                "Failed to serialize template format to YAML",
373                &e.to_string(),
374            )
375        })
376    }
377
378    /// Validate the template format
379    ///
380    /// Validates that the template format is well-formed:
381    /// - Name is not empty
382    /// - Tree contains at least one node
383    /// - All nodes are valid (file nodes have content/template, directories don't)
384    ///
385    /// # Returns
386    ///
387    /// `Ok(())` if the template is valid.
388    ///
389    /// # Errors
390    ///
391    /// Returns an error if validation fails, with a message describing the issue.
392    ///
393    /// # Examples
394    ///
395    /// ## Success case
396    ///
397    /// ```rust
398    /// use crate::templates::format::{TemplateFormat, FileTreeNode};
399    ///
400    /// # fn main() -> crate::utils::error::Result<()> {
401    /// let mut format = TemplateFormat::new("my-template");
402    /// format.add_node(FileTreeNode::directory("src"));
403    ///
404    /// format.validate()?; // Ok
405    /// # Ok(())
406    /// # }
407    /// ```
408    ///
409    /// ## Error case - empty tree
410    ///
411    /// ```rust
412    /// use crate::templates::format::TemplateFormat;
413    ///
414    /// let format = TemplateFormat::new("my-template");
415    /// let result = format.validate();
416    /// assert!(result.is_err());
417    /// ```
418    ///
419    /// ## Error case - invalid file node
420    ///
421    /// ```rust
422    /// use crate::templates::format::{TemplateFormat, FileTreeNode, NodeType};
423    ///
424    /// let mut format = TemplateFormat::new("my-template");
425    /// let mut invalid_file = FileTreeNode {
426    ///     node_type: NodeType::File,
427    ///     name: "test.rs".to_string(),
428    ///     children: vec![],
429    ///     content: None,
430    ///     template: None,
431    ///     mode: None,
432    /// };
433    /// format.add_node(invalid_file);
434    ///
435    /// let result = format.validate();
436    /// assert!(result.is_err());
437    /// ```
438    pub fn validate(&self) -> Result<()> {
439        if self.name.is_empty() {
440            return Err(crate::utils::error::Error::new(
441                "Template name cannot be empty",
442            ));
443        }
444
445        if self.tree.is_empty() {
446            return Err(crate::utils::error::Error::new(
447                "Template must contain at least one tree node",
448            ));
449        }
450
451        Self::validate_nodes(&self.tree)?;
452
453        Ok(())
454    }
455
456    fn validate_nodes(nodes: &[FileTreeNode]) -> Result<()> {
457        for node in nodes {
458            if node.name.is_empty() {
459                return Err(crate::utils::error::Error::new("Node name cannot be empty"));
460            }
461
462            match node.node_type {
463                NodeType::File => {
464                    if node.content.is_none() && node.template.is_none() {
465                        return Err(crate::utils::error::Error::new(&format!(
466                            "File node '{}' must have either content or template",
467                            node.name
468                        )));
469                    }
470                    if !node.children.is_empty() {
471                        return Err(crate::utils::error::Error::new(&format!(
472                            "File node '{}' cannot have children",
473                            node.name
474                        )));
475                    }
476                }
477                NodeType::Directory => {
478                    if node.content.is_some() || node.template.is_some() {
479                        return Err(crate::utils::error::Error::new(&format!(
480                            "Directory node '{}' cannot have content or template",
481                            node.name
482                        )));
483                    }
484                    Self::validate_nodes(&node.children)?;
485                }
486            }
487        }
488        Ok(())
489    }
490}
491
492impl FileTreeNode {
493    /// Create a new directory node
494    ///
495    /// Creates a directory node with no children. Children can be added
496    /// using `add_child()`.
497    ///
498    /// # Arguments
499    ///
500    /// * `name` - Directory name (may contain template variables like `{{ name }}`)
501    ///
502    /// # Returns
503    ///
504    /// A new `FileTreeNode` with `NodeType::Directory`.
505    ///
506    /// # Examples
507    ///
508    /// ```rust
509    /// use crate::templates::format::{FileTreeNode, NodeType};
510    ///
511    /// let dir = FileTreeNode::directory("src");
512    /// assert_eq!(dir.node_type, NodeType::Directory);
513    /// assert_eq!(dir.name, "src");
514    /// assert!(dir.children.is_empty());
515    /// ```
516    pub fn directory(name: impl Into<String>) -> Self {
517        Self {
518            node_type: NodeType::Directory,
519            name: name.into(),
520            children: Vec::new(),
521            content: None,
522            template: None,
523            mode: None,
524        }
525    }
526
527    /// Create a new file node with inline content
528    ///
529    /// Creates a file node with inline content that will be written directly
530    /// to the generated file. The content may contain template variables.
531    ///
532    /// # Arguments
533    ///
534    /// * `name` - File name (may contain template variables)
535    /// * `content` - File content (may contain template variables)
536    ///
537    /// # Returns
538    ///
539    /// A new `FileTreeNode` with `NodeType::File` and inline content.
540    ///
541    /// # Examples
542    ///
543    /// ```rust
544    /// use crate::templates::format::{FileTreeNode, NodeType};
545    ///
546    /// let file = FileTreeNode::file_with_content("main.rs", "fn main() {}");
547    /// assert_eq!(file.node_type, NodeType::File);
548    /// assert_eq!(file.name, "main.rs");
549    /// assert_eq!(file.content, Some("fn main() {}".to_string()));
550    /// ```
551    pub fn file_with_content(name: impl Into<String>, content: impl Into<String>) -> Self {
552        Self {
553            node_type: NodeType::File,
554            name: name.into(),
555            children: Vec::new(),
556            content: Some(content.into()),
557            template: None,
558            mode: None,
559        }
560    }
561
562    /// Create a new file node with template reference
563    ///
564    /// Creates a file node that references an external template file.
565    /// The template will be loaded and rendered during generation.
566    ///
567    /// # Arguments
568    ///
569    /// * `name` - File name (may contain template variables)
570    /// * `template` - Path to template file (relative to template base directory)
571    ///
572    /// # Returns
573    ///
574    /// A new `FileTreeNode` with `NodeType::File` and a template reference.
575    ///
576    /// # Examples
577    ///
578    /// ```rust
579    /// use crate::templates::format::{FileTreeNode, NodeType};
580    ///
581    /// let file = FileTreeNode::file_with_template("lib.rs", "templates/lib.rs.tera");
582    /// assert_eq!(file.node_type, NodeType::File);
583    /// assert_eq!(file.name, "lib.rs");
584    /// assert_eq!(file.template, Some("templates/lib.rs.tera".to_string()));
585    /// ```
586    pub fn file_with_template(name: impl Into<String>, template: impl Into<String>) -> Self {
587        Self {
588            node_type: NodeType::File,
589            name: name.into(),
590            children: Vec::new(),
591            content: None,
592            template: Some(template.into()),
593            mode: None,
594        }
595    }
596
597    /// Add a child node (for directories)
598    ///
599    /// Adds a child node to this directory. Only valid for directory nodes.
600    /// Returns `&mut Self` for method chaining.
601    ///
602    /// # Arguments
603    ///
604    /// * `child` - Child node to add
605    ///
606    /// # Returns
607    ///
608    /// `&mut Self` for method chaining.
609    ///
610    /// # Examples
611    ///
612    /// ```rust
613    /// use crate::templates::format::FileTreeNode;
614    ///
615    /// let mut dir = FileTreeNode::directory("src");
616    /// dir.add_child(FileTreeNode::file_with_content("main.rs", "fn main() {}"));
617    ///
618    /// assert_eq!(dir.children.len(), 1);
619    /// assert_eq!(dir.children[0].name, "main.rs");
620    /// ```
621    pub fn add_child(&mut self, child: FileTreeNode) -> &mut Self {
622        self.children.push(child);
623        self
624    }
625
626    /// Set file permissions
627    ///
628    /// Sets Unix file permissions (mode) for this file node. Only valid for file nodes.
629    /// Returns `Self` for method chaining.
630    ///
631    /// # Arguments
632    ///
633    /// * `mode` - Unix file mode (e.g., `0o755` for executable)
634    ///
635    /// # Returns
636    ///
637    /// `Self` for method chaining.
638    ///
639    /// # Examples
640    ///
641    /// ```rust
642    /// use crate::templates::format::FileTreeNode;
643    ///
644    /// let file = FileTreeNode::file_with_content("script.sh", "#!/bin/bash")
645    ///     .with_mode(0o755);
646    ///
647    /// assert_eq!(file.mode, Some(0o755));
648    /// ```
649    pub fn with_mode(mut self, mode: u32) -> Self {
650        self.mode = Some(mode);
651        self
652    }
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658
659    #[test]
660    fn test_directory_node() {
661        let node = FileTreeNode::directory("src");
662        assert_eq!(node.node_type, NodeType::Directory);
663        assert_eq!(node.name, "src");
664        assert!(node.children.is_empty());
665    }
666
667    #[test]
668    fn test_file_node_with_content() {
669        let node = FileTreeNode::file_with_content("main.rs", "fn main() {}");
670        assert_eq!(node.node_type, NodeType::File);
671        assert_eq!(node.name, "main.rs");
672        assert_eq!(node.content, Some("fn main() {}".to_string()));
673        assert_eq!(node.template, None);
674    }
675
676    #[test]
677    fn test_file_node_with_template() {
678        let node = FileTreeNode::file_with_template("lib.rs", "templates/lib.rs.tera");
679        assert_eq!(node.node_type, NodeType::File);
680        assert_eq!(node.name, "lib.rs");
681        assert_eq!(node.template, Some("templates/lib.rs.tera".to_string()));
682        assert_eq!(node.content, None);
683    }
684
685    #[test]
686    fn test_template_format_creation() {
687        let mut format = TemplateFormat::new("test-template");
688        format.add_variable("service_name");
689        format.add_default("port", "8080");
690
691        assert_eq!(format.name, "test-template");
692        assert_eq!(format.variables, vec!["service_name"]);
693        assert_eq!(format.defaults.get("port"), Some(&"8080".to_string()));
694    }
695
696    #[test]
697    fn test_template_format_validation() {
698        let mut format = TemplateFormat::new("test");
699        format.add_node(FileTreeNode::directory("src"));
700
701        assert!(format.validate().is_ok());
702    }
703
704    #[test]
705    fn test_empty_template_validation_fails() {
706        let format = TemplateFormat::new("test");
707        assert!(format.validate().is_err());
708    }
709}