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