Skip to main content

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(), &timestamp);
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(), &timestamp);
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(), &timestamp);
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}