use std::{collections::BTreeMap, path::PathBuf};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use crate::update_type::UpdateType;
#[derive(Debug, Serialize, Deserialize)]
pub struct ChangePackLog {
changes: BTreeMap<PathBuf, UpdateType>,
note: String,
date: DateTime<Utc>,
}
impl ChangePackLog {
#[must_use]
pub fn new(changes: BTreeMap<PathBuf, UpdateType>, note: String) -> Self {
Self {
changes,
note,
date: Utc::now(),
}
}
#[must_use]
pub fn changes(&self) -> &BTreeMap<PathBuf, UpdateType> {
&self.changes
}
#[must_use]
pub fn note(&self) -> &str {
&self.note
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, path::PathBuf};
use chrono::{DateTime, Utc};
use super::*;
#[test]
fn test_changepack_log_new() {
let mut changes = BTreeMap::new();
changes.insert(
PathBuf::from("packages/foo/package.json"),
UpdateType::Minor,
);
let note = "Add feature X".to_string();
let log = ChangePackLog::new(changes.clone(), note.clone());
assert_eq!(log.changes(), &changes);
assert_eq!(log.note(), note);
}
#[test]
fn test_changepack_log_changes_accessor() {
let mut changes = BTreeMap::new();
changes.insert(
PathBuf::from("packages/foo/package.json"),
UpdateType::Major,
);
changes.insert(PathBuf::from("crates/core/Cargo.toml"), UpdateType::Patch);
let log = ChangePackLog::new(changes.clone(), "Update changes".to_string());
assert_eq!(log.changes().len(), 2);
assert_eq!(log.changes(), &changes);
assert_eq!(
log.changes()
.get(&PathBuf::from("packages/foo/package.json")),
Some(&UpdateType::Major)
);
assert_eq!(
log.changes().get(&PathBuf::from("crates/core/Cargo.toml")),
Some(&UpdateType::Patch)
);
}
#[test]
fn test_changepack_log_note_accessor() {
let mut changes = BTreeMap::new();
changes.insert(
PathBuf::from("packages/foo/package.json"),
UpdateType::Minor,
);
let log = ChangePackLog::new(changes, "Detailed changelog note".to_string());
assert_eq!(log.note(), "Detailed changelog note");
}
#[test]
fn test_changepack_log_empty_changes() {
let log = ChangePackLog::new(BTreeMap::new(), "No package updates".to_string());
assert!(log.changes().is_empty());
assert_eq!(log.note(), "No package updates");
}
#[test]
fn test_changepack_log_serialize_deserialize_roundtrip() {
let mut changes = BTreeMap::new();
changes.insert(
PathBuf::from("packages/foo/package.json"),
UpdateType::Minor,
);
changes.insert(PathBuf::from("crates/core/Cargo.toml"), UpdateType::Patch);
let log = ChangePackLog::new(changes, "Roundtrip changelog note".to_string());
let json = serde_json::to_string(&log).unwrap();
let deserialized: ChangePackLog = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.changes(), log.changes());
assert_eq!(deserialized.note(), log.note());
assert_eq!(deserialized.date, log.date);
}
#[test]
fn test_changepack_log_deserialize_from_json() {
let json = r#"{
"changes": {
"packages/foo/package.json": "Minor",
"crates/core/Cargo.toml": "Patch"
},
"note": "Ship feature and fix",
"date": "2025-12-19T10:27:00.000Z"
}"#;
let log: ChangePackLog = serde_json::from_str(json).unwrap();
let expected_date = DateTime::parse_from_rfc3339("2025-12-19T10:27:00.000Z")
.unwrap()
.with_timezone(&Utc);
assert_eq!(log.changes().len(), 2);
assert_eq!(
log.changes()
.get(&PathBuf::from("packages/foo/package.json")),
Some(&UpdateType::Minor)
);
assert_eq!(
log.changes().get(&PathBuf::from("crates/core/Cargo.toml")),
Some(&UpdateType::Patch)
);
assert_eq!(log.note(), "Ship feature and fix");
assert_eq!(log.date, expected_date);
}
#[test]
fn test_changepack_log_deserialize_ignores_unknown_keys() {
let json = r#"{
"changes": {
"packages/foo/package.json": "Minor"
},
"note": "Written by a newer version",
"date": "2025-12-19T10:27:00.000Z",
"futureField": "some value",
"anotherFutureField": { "nested": [1, 2, 3] }
}"#;
let log: ChangePackLog = serde_json::from_str(json)
.expect("unknown top-level keys must be ignored, not rejected");
assert_eq!(log.changes().len(), 1);
assert_eq!(
log.changes()
.get(&PathBuf::from("packages/foo/package.json")),
Some(&UpdateType::Minor)
);
assert_eq!(log.note(), "Written by a newer version");
}
#[test]
fn test_changepack_log_deterministic_serialization() {
let mut changes = BTreeMap::new();
changes.insert(PathBuf::from("z/package.json"), UpdateType::Major);
changes.insert(PathBuf::from("m/package.json"), UpdateType::Minor);
changes.insert(PathBuf::from("a/package.json"), UpdateType::Patch);
let log = ChangePackLog::new(changes, "Test determinism".to_string());
let json1 = serde_json::to_string(&log).unwrap();
let json2 = serde_json::to_string(&log).unwrap();
assert_eq!(json1, json2, "Serialization should be deterministic");
let deserialized: ChangePackLog = serde_json::from_str(&json1).unwrap();
let keys: Vec<_> = deserialized.changes().keys().collect();
assert_eq!(
keys,
vec![
&PathBuf::from("a/package.json"),
&PathBuf::from("m/package.json"),
&PathBuf::from("z/package.json")
],
"Keys should be in sorted order"
);
}
}