Skip to main content

archelon_core/
entry.rs

1use caretta_id::CarettaId;
2use chrono::NaiveDateTime;
3use serde::{Deserialize, Serialize};
4use std::path::PathBuf;
5
6/// Frontmatter metadata stored at the top of each .md file.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Frontmatter {
9    pub id: CarettaId,
10
11    #[serde(default)]
12    pub title: String,
13
14    /// Optional slug override. If absent, the slug is derived from the filename.
15    #[serde(skip_serializing_if = "Option::is_none")]
16    pub slug: Option<String>,
17
18    /// Timestamp when the entry was first created. Set automatically by `new`.
19    #[serde(default, with = "naive_datetime_serde")]
20    pub created_at: NaiveDateTime,
21
22    /// Timestamp of the last write. Updated automatically by `write_entry`.
23    #[serde(default, with = "naive_datetime_serde")]
24    pub updated_at: NaiveDateTime,
25
26    #[serde(default, skip_serializing_if = "Vec::is_empty")]
27    pub tags: Vec<String>,
28
29    /// Task metadata. Present only when this entry represents a task.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub task: Option<TaskMeta>,
32
33    /// Event metadata. Present only when this entry represents a calendar event.
34    #[serde(skip_serializing_if = "Option::is_none")]
35    pub event: Option<EventMeta>,
36}
37
38/// Task-specific metadata.
39///
40/// Conventional `status` values: `open`, `in_progress`, `done`, `cancelled`, `archived`.
41/// Any custom string is also accepted.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct TaskMeta {
44    /// Due date/time.
45    #[serde(default, skip_serializing_if = "Option::is_none", with = "naive_datetime_serde::eod::opt")]
46    pub due: Option<NaiveDateTime>,
47
48    /// Task status. Conventional values: open | in_progress | done | cancelled | archived
49    #[serde(default = "default_task_status")]
50    pub status: String,
51
52    /// Timestamp when the task was closed (status → done/cancelled/archived).
53    /// Set automatically by `entry set`; can be overridden manually.
54    #[serde(default, skip_serializing_if = "Option::is_none", with = "naive_datetime_serde::opt")]
55    pub closed_at: Option<NaiveDateTime>,
56}
57
58fn default_task_status() -> String {
59    "open".to_owned()
60}
61
62/// Event-specific metadata.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct EventMeta {
65    #[serde(with = "naive_datetime_serde")]
66    pub start: NaiveDateTime,
67    #[serde(with = "naive_datetime_serde::eod")]
68    pub end: NaiveDateTime,
69}
70
71/// A single entry — one Markdown file in the journal.
72/// Tasks and notes coexist freely in the body (bullet-journal style).
73#[derive(Debug, Clone)]
74pub struct Entry {
75    /// Absolute path to the source .md file.
76    pub path: PathBuf,
77
78    /// Parsed frontmatter. Defaults to empty if the file has none.
79    pub frontmatter: Frontmatter,
80
81    /// Raw Markdown body (everything after the frontmatter block).
82    pub body: String,
83}
84
85impl Entry {
86    /// Returns the CarettaId from the frontmatter.
87    pub fn id(&self) -> CarettaId {
88        self.frontmatter.id
89    }
90
91    /// Returns the title: frontmatter title (if non-empty) → file stem → "(untitled)".
92    pub fn title(&self) -> &str {
93        return &self.frontmatter.title;
94    }
95}
96
97/// Custom serde module for `NaiveDateTime` using minute-precision format (`YYYY-MM-DDTHH:MM`).
98///
99/// Serializes to `%Y-%m-%dT%H:%M`. Deserializes from:
100/// - `%Y-%m-%dT%H:%M`        (minute precision — preferred)
101/// - `%Y-%m-%dT%H:%M:%S`     (second precision)
102/// - `%Y-%m-%dT%H:%M:%S%.f`  (sub-second precision — for backward compat)
103/// - `%Y-%m-%d`              (date only — `00:00` for start fields, `23:59` via `eod` sub-module)
104mod naive_datetime_serde {
105    use chrono::{NaiveDate, NaiveDateTime};
106    use serde::{Deserialize, Deserializer, Serializer};
107
108    const FORMAT: &str = "%Y-%m-%dT%H:%M";
109
110    /// Parse a datetime string, using `(h, m)` as the fallback time when only a date is given.
111    pub(super) fn parse_with_fallback(s: &str, h: u32, m: u32) -> Result<NaiveDateTime, String> {
112        for fmt in [FORMAT, "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S%.f"] {
113            if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
114                return Ok(dt);
115            }
116        }
117        if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
118            return Ok(d.and_hms_opt(h, m, 0).unwrap());
119        }
120        Err(format!(
121            "cannot parse `{s}` as a datetime; expected format YYYY-MM-DDTHH:MM"
122        ))
123    }
124
125    pub fn serialize<S: Serializer>(dt: &NaiveDateTime, s: S) -> Result<S::Ok, S::Error> {
126        s.serialize_str(&dt.format(FORMAT).to_string())
127    }
128
129    /// Deserializes with date-only → `00:00` (start-of-day).
130    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<NaiveDateTime, D::Error> {
131        let raw = String::deserialize(d)?;
132        parse_with_fallback(&raw, 0, 0).map_err(serde::de::Error::custom)
133    }
134
135    /// For `Option<NaiveDateTime>` fields — date-only → `00:00`.
136    pub mod opt {
137        use chrono::NaiveDateTime;
138        use serde::{Deserialize, Deserializer, Serializer};
139
140        pub fn serialize<S: Serializer>(
141            opt: &Option<NaiveDateTime>,
142            s: S,
143        ) -> Result<S::Ok, S::Error> {
144            match opt {
145                Some(dt) => super::serialize(dt, s),
146                None => s.serialize_none(),
147            }
148        }
149
150        pub fn deserialize<'de, D: Deserializer<'de>>(
151            d: D,
152        ) -> Result<Option<NaiveDateTime>, D::Error> {
153            let raw = String::deserialize(d)?;
154            super::parse_with_fallback(&raw, 0, 0).map(Some).map_err(serde::de::Error::custom)
155        }
156    }
157
158    /// Variant where date-only → `23:59` (end-of-day). Used for due dates and event end times.
159    pub mod eod {
160        use chrono::NaiveDateTime;
161        use serde::{Deserialize, Deserializer, Serializer};
162
163        pub fn serialize<S: Serializer>(dt: &NaiveDateTime, s: S) -> Result<S::Ok, S::Error> {
164            super::serialize(dt, s)
165        }
166
167        pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<NaiveDateTime, D::Error> {
168            let raw = String::deserialize(d)?;
169            super::parse_with_fallback(&raw, 23, 59).map_err(serde::de::Error::custom)
170        }
171
172        pub mod opt {
173            use chrono::NaiveDateTime;
174            use serde::{Deserialize, Deserializer, Serializer};
175
176            pub fn serialize<S: Serializer>(
177                opt: &Option<NaiveDateTime>,
178                s: S,
179            ) -> Result<S::Ok, S::Error> {
180                match opt {
181                    Some(dt) => super::serialize(dt, s),
182                    None => s.serialize_none(),
183                }
184            }
185
186            pub fn deserialize<'de, D: Deserializer<'de>>(
187                d: D,
188            ) -> Result<Option<NaiveDateTime>, D::Error> {
189                let raw = String::deserialize(d)?;
190                super::super::parse_with_fallback(&raw, 23, 59)
191                    .map(Some)
192                    .map_err(serde::de::Error::custom)
193            }
194        }
195    }
196}