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}