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}