torrust_tracker_deployer_lib/infrastructure/templating/metadata.rs
1//! Template generation metadata for rendered configuration files.
2//!
3//! This module provides the `TemplateMetadata` struct that captures information about
4//! when templates were generated. This metadata is embedded in rendered templates
5//! to provide context for AI agents, developers, and system administrators.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize, Serializer};
9
10/// Serializes `DateTime<Utc>` as ISO 8601 string for Tera templates
11fn serialize_datetime_as_iso8601<S>(dt: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
12where
13 S: Serializer,
14{
15 serializer.serialize_str(&dt.format("%Y-%m-%dT%H:%M:%SZ").to_string())
16}
17
18/// Metadata about template generation.
19///
20/// This struct is designed to be flattened into template contexts using `#[serde(flatten)]`,
21/// making the timestamp available at the top level in templates.
22///
23/// # Example
24///
25/// ```rust
26/// use torrust_tracker_deployer_lib::infrastructure::templating::metadata::TemplateMetadata;
27/// use chrono::{TimeZone, Utc};
28///
29/// let timestamp = Utc.with_ymd_and_hms(2026, 1, 27, 14, 30, 0).unwrap();
30/// let metadata = TemplateMetadata::new(timestamp);
31/// assert_eq!(metadata.generated_at(), ×tamp);
32/// ```
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
34pub struct TemplateMetadata {
35 /// Timestamp when the template was generated (UTC).
36 #[serde(serialize_with = "serialize_datetime_as_iso8601")]
37 generated_at: DateTime<Utc>,
38}
39
40impl TemplateMetadata {
41 /// Creates a new `TemplateMetadata` with the given timestamp.
42 ///
43 /// # Arguments
44 ///
45 /// * `generated_at` - UTC timestamp when the template was generated
46 ///
47 /// # Example
48 ///
49 /// ```rust
50 /// use torrust_tracker_deployer_lib::infrastructure::templating::metadata::TemplateMetadata;
51 /// use chrono::{TimeZone, Utc};
52 ///
53 /// let timestamp = Utc.with_ymd_and_hms(2026, 1, 27, 14, 30, 0).unwrap();
54 /// let metadata = TemplateMetadata::new(timestamp);
55 /// ```
56 #[must_use]
57 pub fn new(generated_at: DateTime<Utc>) -> Self {
58 Self { generated_at }
59 }
60
61 /// Returns the generation timestamp.
62 ///
63 /// # Example
64 ///
65 /// ```rust
66 /// use torrust_tracker_deployer_lib::infrastructure::templating::metadata::TemplateMetadata;
67 /// use chrono::{TimeZone, Utc};
68 ///
69 /// let timestamp = Utc.with_ymd_and_hms(2026, 1, 27, 14, 30, 0).unwrap();
70 /// let metadata = TemplateMetadata::new(timestamp);
71 /// assert_eq!(metadata.generated_at(), ×tamp);
72 /// ```
73 #[must_use]
74 pub fn generated_at(&self) -> &DateTime<Utc> {
75 &self.generated_at
76 }
77
78 /// Returns the timestamp formatted as ISO 8601 string.
79 ///
80 /// Format: `YYYY-MM-DDTHH:MM:SSZ` (e.g., `2026-01-27T14:30:00Z`)
81 ///
82 /// # Example
83 ///
84 /// ```rust
85 /// use torrust_tracker_deployer_lib::infrastructure::templating::metadata::TemplateMetadata;
86 /// use chrono::{TimeZone, Utc};
87 ///
88 /// let timestamp = Utc.with_ymd_and_hms(2026, 1, 27, 14, 30, 0).unwrap();
89 /// let metadata = TemplateMetadata::new(timestamp);
90 /// assert_eq!(metadata.generated_at_iso8601(), "2026-01-27T14:30:00Z");
91 /// ```
92 #[must_use]
93 pub fn generated_at_iso8601(&self) -> String {
94 self.generated_at.format("%Y-%m-%dT%H:%M:%SZ").to_string()
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use chrono::TimeZone;
102
103 #[test]
104 fn it_should_create_template_metadata_with_timestamp() {
105 let timestamp = Utc.with_ymd_and_hms(2026, 1, 27, 14, 30, 0).unwrap();
106 let metadata = TemplateMetadata::new(timestamp);
107
108 assert_eq!(metadata.generated_at(), ×tamp);
109 }
110
111 #[test]
112 fn it_should_format_timestamp_as_iso8601() {
113 let timestamp = Utc.with_ymd_and_hms(2026, 1, 27, 14, 30, 0).unwrap();
114 let metadata = TemplateMetadata::new(timestamp);
115
116 assert_eq!(metadata.generated_at_iso8601(), "2026-01-27T14:30:00Z");
117 }
118
119 #[test]
120 fn it_should_serialize_metadata_correctly() {
121 let timestamp = Utc.with_ymd_and_hms(2026, 1, 27, 14, 30, 0).unwrap();
122 let metadata = TemplateMetadata::new(timestamp);
123 let json = serde_json::to_string(&metadata).unwrap();
124
125 assert!(json.contains("\"generated_at\""));
126 assert!(json.contains("2026-01-27T14:30:00Z"));
127 }
128
129 #[test]
130 fn it_should_deserialize_metadata_correctly() {
131 let json = r#"{"generated_at":"2026-01-27T14:30:00Z"}"#;
132 let metadata: TemplateMetadata = serde_json::from_str(json).unwrap();
133
134 assert_eq!(
135 metadata.generated_at(),
136 &Utc.with_ymd_and_hms(2026, 1, 27, 14, 30, 0).unwrap()
137 );
138 }
139
140 #[test]
141 fn it_should_implement_clone() {
142 let timestamp = Utc.with_ymd_and_hms(2026, 1, 27, 14, 30, 0).unwrap();
143 let metadata = TemplateMetadata::new(timestamp);
144 let cloned = metadata.clone();
145
146 assert_eq!(metadata, cloned);
147 }
148}