//! Calendar JSON generation — counts posts by year, month, day. use std::collections::BTreeMap; use std::path::Path; use crate::db::DbConnection as Connection; use crate::db::queries::post as post_q; use crate::engine::EngineResult; use crate::model::PostStatus; use crate::util::{atomic_write, timestamp}; /// Generate `html/calendar.json` from published posts. /// /// The JSON has three top-level objects: `years`, `months`, `days`, /// each mapping a date key to a count of posts. pub fn regenerate_calendar( conn: &Connection, data_dir: &Path, project_id: &str, ) -> EngineResult<()> { let posts = post_q::list_posts_by_project(conn, project_id)?; let mut years: BTreeMap = BTreeMap::new(); let mut months: BTreeMap = BTreeMap::new(); let mut days: BTreeMap = BTreeMap::new(); for post in &posts { if post.status != PostStatus::Published { continue; } let ts = post.created_at; let (y, m, d) = timestamp::year_month_day_from_unix_ms(ts); let year_key = y.clone(); let month_key = format!("{y}-{m}"); let day_key = format!("{y}-{m}-{d}"); *years.entry(year_key).or_insert(0) += 1; *months.entry(month_key).or_insert(0) += 1; *days.entry(day_key).or_insert(0) += 1; } let calendar = serde_json::json!({ "years": years, "months": months, "days": days, }); let html_dir = data_dir.join("html"); std::fs::create_dir_all(&html_dir)?; let json_str = serde_json::to_string_pretty(&calendar)?; atomic_write::atomic_write(&html_dir.join("calendar.json"), json_str.as_bytes())?; Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::db::Database; use crate::engine; #[test] fn calendar_empty_project() { let db = Database::open_in_memory().unwrap(); let _ = db.migrate(); let tmp = tempfile::tempdir().unwrap(); let p = engine::project::create_project(db.conn(), "Test", Some(tmp.path().to_str().unwrap())) .unwrap(); regenerate_calendar(db.conn(), tmp.path(), &p.id).unwrap(); let cal_path = tmp.path().join("html").join("calendar.json"); assert!(cal_path.exists()); let data: serde_json::Value = serde_json::from_str(&std::fs::read_to_string(&cal_path).unwrap()).unwrap(); assert!(data["years"].as_object().unwrap().is_empty()); } }