Skip to main content

changepacks_core/
update_log.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use crate::update_type::UpdateType;
7
8/// On-disk changepack log entry with changes map, note, and timestamp.
9///
10/// Stored in `.changepacks/changepack_log_*.json` files and used to calculate
11/// version updates during the update command.
12#[derive(Debug, Serialize, Deserialize)]
13pub struct ChangePackLog {
14    /// Map of package file paths to their update types
15    changes: HashMap<PathBuf, UpdateType>,
16    /// User-provided changelog note for this changepack
17    note: String,
18    /// UTC timestamp when this changepack was created
19    date: DateTime<Utc>,
20}
21
22impl ChangePackLog {
23    #[must_use]
24    pub fn new(changes: HashMap<PathBuf, UpdateType>, note: String) -> Self {
25        Self {
26            changes,
27            note,
28            date: Utc::now(),
29        }
30    }
31
32    #[must_use]
33    pub fn changes(&self) -> &HashMap<PathBuf, UpdateType> {
34        &self.changes
35    }
36
37    #[must_use]
38    pub fn note(&self) -> &str {
39        &self.note
40    }
41}
42
43#[cfg(test)]
44mod tests {
45    use std::{collections::HashMap, path::PathBuf};
46
47    use chrono::{DateTime, Utc};
48
49    use super::*;
50
51    #[test]
52    fn test_changepack_log_new() {
53        let mut changes = HashMap::new();
54        changes.insert(
55            PathBuf::from("packages/foo/package.json"),
56            UpdateType::Minor,
57        );
58        let note = "Add feature X".to_string();
59
60        let log = ChangePackLog::new(changes.clone(), note.clone());
61
62        assert_eq!(log.changes(), &changes);
63        assert_eq!(log.note(), note);
64    }
65
66    #[test]
67    fn test_changepack_log_changes_accessor() {
68        let mut changes = HashMap::new();
69        changes.insert(
70            PathBuf::from("packages/foo/package.json"),
71            UpdateType::Major,
72        );
73        changes.insert(PathBuf::from("crates/core/Cargo.toml"), UpdateType::Patch);
74
75        let log = ChangePackLog::new(changes.clone(), "Update changes".to_string());
76
77        assert_eq!(log.changes().len(), 2);
78        assert_eq!(log.changes(), &changes);
79        assert_eq!(
80            log.changes()
81                .get(&PathBuf::from("packages/foo/package.json")),
82            Some(&UpdateType::Major)
83        );
84        assert_eq!(
85            log.changes().get(&PathBuf::from("crates/core/Cargo.toml")),
86            Some(&UpdateType::Patch)
87        );
88    }
89
90    #[test]
91    fn test_changepack_log_note_accessor() {
92        let mut changes = HashMap::new();
93        changes.insert(
94            PathBuf::from("packages/foo/package.json"),
95            UpdateType::Minor,
96        );
97
98        let log = ChangePackLog::new(changes, "Detailed changelog note".to_string());
99
100        assert_eq!(log.note(), "Detailed changelog note");
101    }
102
103    #[test]
104    fn test_changepack_log_empty_changes() {
105        let log = ChangePackLog::new(HashMap::new(), "No package updates".to_string());
106
107        assert!(log.changes().is_empty());
108        assert_eq!(log.note(), "No package updates");
109    }
110
111    #[test]
112    fn test_changepack_log_serialize_deserialize_roundtrip() {
113        let mut changes = HashMap::new();
114        changes.insert(
115            PathBuf::from("packages/foo/package.json"),
116            UpdateType::Minor,
117        );
118        changes.insert(PathBuf::from("crates/core/Cargo.toml"), UpdateType::Patch);
119        let log = ChangePackLog::new(changes, "Roundtrip changelog note".to_string());
120
121        let json = serde_json::to_string(&log).unwrap();
122        let deserialized: ChangePackLog = serde_json::from_str(&json).unwrap();
123
124        assert_eq!(deserialized.changes(), log.changes());
125        assert_eq!(deserialized.note(), log.note());
126        assert_eq!(deserialized.date, log.date);
127    }
128
129    #[test]
130    fn test_changepack_log_deserialize_from_json() {
131        let json = r#"{
132            "changes": {
133                "packages/foo/package.json": "Minor",
134                "crates/core/Cargo.toml": "Patch"
135            },
136            "note": "Ship feature and fix",
137            "date": "2025-12-19T10:27:00.000Z"
138        }"#;
139
140        let log: ChangePackLog = serde_json::from_str(json).unwrap();
141        let expected_date = DateTime::parse_from_rfc3339("2025-12-19T10:27:00.000Z")
142            .unwrap()
143            .with_timezone(&Utc);
144
145        assert_eq!(log.changes().len(), 2);
146        assert_eq!(
147            log.changes()
148                .get(&PathBuf::from("packages/foo/package.json")),
149            Some(&UpdateType::Minor)
150        );
151        assert_eq!(
152            log.changes().get(&PathBuf::from("crates/core/Cargo.toml")),
153            Some(&UpdateType::Patch)
154        );
155        assert_eq!(log.note(), "Ship feature and fix");
156        assert_eq!(log.date, expected_date);
157    }
158}