1use caretta_id::CarettaId;
2use chrono::NaiveDateTime;
3use indexmap::IndexMap;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7use crate::labels::{EntryFlag, entry_flags};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct Frontmatter {
12 pub id: CarettaId,
13
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub parent_id: Option<CarettaId>,
17
18 #[serde(default)]
19 pub title: String,
20
21 #[serde(default, skip_serializing_if = "String::is_empty")]
23 pub slug: String,
24
25 #[serde(default, with = "naive_datetime_serde")]
27 pub created_at: NaiveDateTime,
28
29 #[serde(default, with = "naive_datetime_serde")]
31 pub updated_at: NaiveDateTime,
32
33 #[serde(default, skip_serializing_if = "Vec::is_empty")]
34 pub tags: Vec<String>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub task: Option<TaskMeta>,
39
40 #[serde(skip_serializing_if = "Option::is_none")]
42 pub event: Option<EventMeta>,
43
44 #[serde(flatten)]
46 pub extra: IndexMap<String, serde_yaml::Value>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct TaskMeta {
55 #[serde(default, skip_serializing_if = "Option::is_none", with = "naive_datetime_serde::eod::opt")]
57 pub due: Option<NaiveDateTime>,
58
59 #[serde(default = "default_task_status")]
61 pub status: String,
62
63 #[serde(default, skip_serializing_if = "Option::is_none", with = "naive_datetime_serde::opt")]
66 pub started_at: Option<NaiveDateTime>,
67
68 #[serde(default, skip_serializing_if = "Option::is_none", with = "naive_datetime_serde::opt")]
71 pub closed_at: Option<NaiveDateTime>,
72
73 #[serde(flatten)]
75 pub extra: IndexMap<String, serde_yaml::Value>,
76}
77
78fn default_task_status() -> String {
79 "open".to_owned()
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct EventMeta {
85 #[serde(with = "naive_datetime_serde")]
86 pub start: NaiveDateTime,
87 #[serde(with = "naive_datetime_serde::eod")]
88 pub end: NaiveDateTime,
89
90 #[serde(flatten)]
92 pub extra: IndexMap<String, serde_yaml::Value>,
93}
94
95#[derive(Debug, Clone)]
98pub struct Entry {
99 pub path: PathBuf,
101
102 pub frontmatter: Frontmatter,
104
105 pub body: String,
107}
108
109impl Entry {
110 pub fn id(&self) -> CarettaId {
112 self.frontmatter.id
113 }
114
115 pub fn title(&self) -> &str {
117 return &self.frontmatter.title;
118 }
119}
120
121#[derive(Debug, Clone, Serialize)]
125pub struct TaskMetaView {
126 #[serde(skip_serializing_if = "Option::is_none", with = "naive_datetime_serde::eod::opt")]
127 pub due: Option<NaiveDateTime>,
128 pub status: String,
129 #[serde(skip_serializing_if = "Option::is_none", with = "naive_datetime_serde::opt")]
130 pub started_at: Option<NaiveDateTime>,
131 #[serde(skip_serializing_if = "Option::is_none", with = "naive_datetime_serde::opt")]
132 pub closed_at: Option<NaiveDateTime>,
133}
134
135impl From<TaskMeta> for TaskMetaView {
136 fn from(t: TaskMeta) -> Self {
137 TaskMetaView { due: t.due, status: t.status, started_at: t.started_at, closed_at: t.closed_at }
138 }
139}
140
141#[derive(Debug, Clone, Serialize)]
145pub struct EventMetaView {
146 #[serde(with = "naive_datetime_serde")]
147 pub start: NaiveDateTime,
148 #[serde(with = "naive_datetime_serde::eod")]
149 pub end: NaiveDateTime,
150}
151
152impl From<EventMeta> for EventMetaView {
153 fn from(e: EventMeta) -> Self {
154 EventMetaView { start: e.start, end: e.end }
155 }
156}
157
158#[derive(Debug, Clone, Serialize)]
163pub struct FrontmatterView {
164 pub id: CarettaId,
165 #[serde(skip)]
166 pub parent_id: Option<CarettaId>,
167 pub title: String,
168 pub slug: String,
169 #[serde(with = "naive_datetime_serde")]
170 pub created_at: NaiveDateTime,
171 #[serde(with = "naive_datetime_serde")]
172 pub updated_at: NaiveDateTime,
173 pub tags: Vec<String>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub task: Option<TaskMetaView>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub event: Option<EventMetaView>,
178}
179
180impl From<Frontmatter> for FrontmatterView {
181 fn from(fm: Frontmatter) -> Self {
182 FrontmatterView {
183 id: fm.id,
184 parent_id: fm.parent_id,
185 title: fm.title,
186 slug: fm.slug,
187 created_at: fm.created_at,
188 updated_at: fm.updated_at,
189 tags: fm.tags,
190 task: fm.task.map(TaskMetaView::from),
191 event: fm.event.map(EventMetaView::from),
192 }
193 }
194}
195
196#[derive(Debug, Clone, Serialize)]
201pub struct EntryHeader {
202 pub path: String,
203 #[serde(flatten)]
204 pub frontmatter: FrontmatterView,
205 pub flags: Vec<EntryFlag>,
207}
208
209impl EntryHeader {
210 pub fn id(&self) -> CarettaId {
211 self.frontmatter.id
212 }
213
214 pub fn title(&self) -> &str {
215 &self.frontmatter.title
216 }
217}
218
219impl From<Entry> for EntryHeader {
220 fn from(entry: Entry) -> Self {
221 let fm = FrontmatterView::from(entry.frontmatter);
222 let flags = entry_flags(fm.task.as_ref(), fm.event.as_ref(), fm.created_at, fm.updated_at);
223 EntryHeader { path: entry.path.to_string_lossy().into_owned(), frontmatter: fm, flags }
224 }
225}
226
227mod naive_datetime_serde {
235 use chrono::{NaiveDate, NaiveDateTime};
236 use serde::{Deserialize, Deserializer, Serializer};
237
238 const FORMAT: &str = "%Y-%m-%dT%H:%M";
239
240 pub(super) fn parse_with_fallback(s: &str, h: u32, m: u32) -> Result<NaiveDateTime, String> {
242 for fmt in [FORMAT, "%Y-%m-%dT%H:%M:%S", "%Y-%m-%dT%H:%M:%S%.f"] {
243 if let Ok(dt) = NaiveDateTime::parse_from_str(s, fmt) {
244 return Ok(dt);
245 }
246 }
247 if let Ok(d) = NaiveDate::parse_from_str(s, "%Y-%m-%d") {
248 return Ok(d.and_hms_opt(h, m, 0).unwrap());
249 }
250 Err(format!(
251 "cannot parse `{s}` as a datetime; expected format YYYY-MM-DDTHH:MM"
252 ))
253 }
254
255 pub fn serialize<S: Serializer>(dt: &NaiveDateTime, s: S) -> Result<S::Ok, S::Error> {
256 s.serialize_str(&dt.format(FORMAT).to_string())
257 }
258
259 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<NaiveDateTime, D::Error> {
261 let raw = String::deserialize(d)?;
262 parse_with_fallback(&raw, 0, 0).map_err(serde::de::Error::custom)
263 }
264
265 pub mod opt {
267 use chrono::NaiveDateTime;
268 use serde::{Deserialize, Deserializer, Serializer};
269
270 pub fn serialize<S: Serializer>(
271 opt: &Option<NaiveDateTime>,
272 s: S,
273 ) -> Result<S::Ok, S::Error> {
274 match opt {
275 Some(dt) => super::serialize(dt, s),
276 None => s.serialize_none(),
277 }
278 }
279
280 pub fn deserialize<'de, D: Deserializer<'de>>(
281 d: D,
282 ) -> Result<Option<NaiveDateTime>, D::Error> {
283 let raw = String::deserialize(d)?;
284 super::parse_with_fallback(&raw, 0, 0).map(Some).map_err(serde::de::Error::custom)
285 }
286 }
287
288 pub mod eod {
290 use chrono::NaiveDateTime;
291 use serde::{Deserialize, Deserializer, Serializer};
292
293 pub fn serialize<S: Serializer>(dt: &NaiveDateTime, s: S) -> Result<S::Ok, S::Error> {
294 super::serialize(dt, s)
295 }
296
297 pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<NaiveDateTime, D::Error> {
298 let raw = String::deserialize(d)?;
299 super::parse_with_fallback(&raw, 23, 59).map_err(serde::de::Error::custom)
300 }
301
302 pub mod opt {
303 use chrono::NaiveDateTime;
304 use serde::{Deserialize, Deserializer, Serializer};
305
306 pub fn serialize<S: Serializer>(
307 opt: &Option<NaiveDateTime>,
308 s: S,
309 ) -> Result<S::Ok, S::Error> {
310 match opt {
311 Some(dt) => super::serialize(dt, s),
312 None => s.serialize_none(),
313 }
314 }
315
316 pub fn deserialize<'de, D: Deserializer<'de>>(
317 d: D,
318 ) -> Result<Option<NaiveDateTime>, D::Error> {
319 let raw = String::deserialize(d)?;
320 super::super::parse_with_fallback(&raw, 23, 59)
321 .map(Some)
322 .map_err(serde::de::Error::custom)
323 }
324 }
325 }
326}