Skip to main content

mps/elements/
mod.rs

1//! Element type system.
2//!
3//! [`Element`] is an enum with one variant per known element type.
4//! [`split_args`] parses `"work, status: done"` into tags + attrs,
5//! mirroring Ruby's `Element.split_args`.
6
7use std::collections::HashMap;
8
9pub mod task;
10pub mod note;
11pub mod log_elem;
12pub mod reminder;
13pub mod mps_group;
14pub mod character;
15
16pub use task::TaskData;
17pub use note::NoteData;
18pub use log_elem::LogData;
19pub use reminder::ReminderData;
20pub use mps_group::MpsGroupData;
21pub use character::CharacterData;
22
23/// Parsed args from a raw args string like "work, release, status: done".
24/// Parts with ':' become attrs; bare words become tags.
25#[derive(Debug, Clone, Default)]
26pub struct ParsedArgs {
27    pub attrs: HashMap<String, String>,
28    pub tags:  Vec<String>,
29}
30
31/// Split "work, release, status: done" into tags + attrs.
32/// Mirrors Ruby's Element.split_args exactly.
33pub fn split_args(raw: &str) -> ParsedArgs {
34    let mut attrs = HashMap::new();
35    let mut tags  = Vec::new();
36    if raw.trim().is_empty() {
37        return ParsedArgs::default();
38    }
39    for part in raw.split(',') {
40        let part = part.trim();
41        if part.is_empty() { continue; }
42        if let Some(colon) = part.find(':') {
43            let key = part[..colon].trim().to_string();
44            let val = part[colon + 1..].trim().to_string();
45            attrs.insert(key, val);
46        } else {
47            tags.push(part.to_string());
48        }
49    }
50    ParsedArgs { attrs, tags }
51}
52
53/// Discriminant for filtering without matching the full variant.
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
55pub enum ElementKind {
56    Task,
57    Note,
58    Log,
59    Reminder,
60    MpsGroup,
61    Character,
62    Unknown,
63}
64
65impl std::fmt::Display for ElementKind {
66    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67        match self {
68            ElementKind::Task      => write!(f, "task"),
69            ElementKind::Note      => write!(f, "note"),
70            ElementKind::Log       => write!(f, "log"),
71            ElementKind::Reminder  => write!(f, "reminder"),
72            ElementKind::MpsGroup  => write!(f, "mps"),
73            ElementKind::Character => write!(f, "character"),
74            ElementKind::Unknown   => write!(f, "unknown"),
75        }
76    }
77}
78
79impl ElementKind {
80    pub fn from_sign(sign: &str) -> Self {
81        match sign {
82            "task"      => ElementKind::Task,
83            "note"      => ElementKind::Note,
84            "log"       => ElementKind::Log,
85            "reminder"  => ElementKind::Reminder,
86            "mps"       => ElementKind::MpsGroup,
87            "character" => ElementKind::Character,
88            _           => ElementKind::Unknown,
89        }
90    }
91}
92
93#[allow(dead_code)]
94/// All element types the parser can produce.
95/// Using an enum (not trait objects): exhaustive matching, no heap allocation per element,
96/// and pattern matching is idiomatic for the display/filter/export branches.
97#[derive(Debug, Clone)]
98pub enum Element {
99    Task {
100        raw_args: String,
101        refs:     Vec<u64>,
102        body_str: String,
103        data:     TaskData,
104    },
105    Note {
106        raw_args: String,
107        refs:     Vec<u64>,
108        body_str: String,
109        data:     NoteData,
110    },
111    Log {
112        raw_args: String,
113        refs:     Vec<u64>,
114        body_str: String,
115        data:     LogData,
116    },
117    Reminder {
118        raw_args: String,
119        refs:     Vec<u64>,
120        body_str: String,
121        data:     ReminderData,
122    },
123    MpsGroup {
124        raw_args: String,
125        refs:     Vec<u64>,
126        body_str: String,
127        data:     MpsGroupData,
128    },
129    Character {
130        raw_args: String,
131        refs:     Vec<u64>,
132        body_str: String,
133        data:     CharacterData,
134    },
135    Unknown {
136        sign:     String,
137        raw_args: String,
138        refs:     Vec<u64>,
139        body_str: String,
140    },
141}
142
143impl Element {
144    pub fn kind(&self) -> ElementKind {
145        match self {
146            Element::Task      { .. } => ElementKind::Task,
147            Element::Note      { .. } => ElementKind::Note,
148            Element::Log       { .. } => ElementKind::Log,
149            Element::Reminder  { .. } => ElementKind::Reminder,
150            Element::MpsGroup  { .. } => ElementKind::MpsGroup,
151            Element::Character { .. } => ElementKind::Character,
152            Element::Unknown   { .. } => ElementKind::Unknown,
153        }
154    }
155
156    pub fn is_mps_group(&self) -> bool { matches!(self, Element::MpsGroup { .. }) }
157
158    pub fn tags(&self) -> &[String] {
159        match self {
160            Element::Task      { data, .. } => &data.tags,
161            Element::Note      { data, .. } => &data.tags,
162            Element::Log       { data, .. } => &data.tags,
163            Element::Reminder  { data, .. } => &data.tags,
164            Element::MpsGroup  { data, .. } => &data.tags,
165            Element::Character { data, .. } => &data.tags,
166            Element::Unknown   { .. }       => &[],
167        }
168    }
169
170    pub fn body_str(&self) -> &str {
171        match self {
172            Element::Task      { body_str, .. } => body_str,
173            Element::Note      { body_str, .. } => body_str,
174            Element::Log       { body_str, .. } => body_str,
175            Element::Reminder  { body_str, .. } => body_str,
176            Element::MpsGroup  { body_str, .. } => body_str,
177            Element::Character { body_str, .. } => body_str,
178            Element::Unknown   { body_str, .. } => body_str,
179        }
180    }
181
182    #[allow(dead_code)]
183    pub fn refs(&self) -> &[u64] {
184        match self {
185            Element::Task      { refs, .. } => refs,
186            Element::Note      { refs, .. } => refs,
187            Element::Log       { refs, .. } => refs,
188            Element::Reminder  { refs, .. } => refs,
189            Element::MpsGroup  { refs, .. } => refs,
190            Element::Character { refs, .. } => refs,
191            Element::Unknown   { refs, .. } => refs,
192        }
193    }
194
195    pub fn sign(&self) -> &str {
196        match self {
197            Element::Task      { .. }       => "task",
198            Element::Note      { .. }       => "note",
199            Element::Log       { .. }       => "log",
200            Element::Reminder  { .. }       => "reminder",
201            Element::MpsGroup  { .. }       => "mps",
202            Element::Character { .. }       => "character",
203            Element::Unknown   { sign, .. } => sign,
204        }
205    }
206
207    /// The raw args string as it appeared in the source file (e.g. "work, status: open").
208    pub fn raw_args(&self) -> &str {
209        match self {
210            Element::Task      { raw_args, .. } => raw_args,
211            Element::Note      { raw_args, .. } => raw_args,
212            Element::Log       { raw_args, .. } => raw_args,
213            Element::Reminder  { raw_args, .. } => raw_args,
214            Element::MpsGroup  { raw_args, .. } => raw_args,
215            Element::Character { raw_args, .. } => raw_args,
216            Element::Unknown   { raw_args, .. } => raw_args,
217        }
218    }
219
220    /// Typed (named) attributes, excluding tags. Used by rewrite_element to merge new attrs.
221    /// Only includes attributes that were actually set (no defaults for absent optional attrs).
222    pub fn typed_attrs(&self) -> Vec<(String, String)> {
223        match self {
224            // Task always has a status (default "open") — include it always.
225            Element::Task { data, .. } => vec![
226                ("status".into(), data.status_str().into()),
227            ],
228            Element::Log { data, .. } => {
229                let mut attrs = Vec::new();
230                if let Some(ref s) = data.start { attrs.push(("start".into(), s.clone())); }
231                if let Some(ref e) = data.end   { attrs.push(("end".into(),   e.clone())); }
232                attrs
233            }
234            Element::Reminder { data, .. } => {
235                if let Some(ref a) = data.at {
236                    vec![("at".into(), a.clone())]
237                } else {
238                    Vec::new()
239                }
240            }
241            Element::Character { data, .. } => {
242                if let Some(ref n) = data.name {
243                    vec![("name".into(), n.clone())]
244                } else {
245                    Vec::new()
246                }
247            }
248            _ => Vec::new(),
249        }
250    }
251
252    /// True if this element is an Unknown type (sign not recognised by the parser).
253    pub fn is_unknown(&self) -> bool { matches!(self, Element::Unknown { .. }) }
254
255    /// Build an Element from a parsed sign, raw_args, refs, and body_str.
256    pub fn from_parts(sign: &str, raw_args: String, refs: Vec<u64>, body_str: String) -> Self {
257        match sign {
258            "task" => Element::Task {
259                data: TaskData::parse_args(&raw_args),
260                raw_args, refs, body_str,
261            },
262            "note" => Element::Note {
263                data: NoteData::parse_args(&raw_args),
264                raw_args, refs, body_str,
265            },
266            "log" => Element::Log {
267                data: LogData::parse_args(&raw_args),
268                raw_args, refs, body_str,
269            },
270            "reminder" => Element::Reminder {
271                data: ReminderData::parse_args(&raw_args),
272                raw_args, refs, body_str,
273            },
274            "mps" => Element::MpsGroup {
275                data: MpsGroupData::parse_args(&raw_args),
276                raw_args, refs, body_str,
277            },
278            "character" => Element::Character {
279                data: CharacterData::parse_args(&raw_args),
280                raw_args, refs, body_str,
281            },
282            other => Element::Unknown {
283                sign: other.to_string(),
284                raw_args, refs, body_str,
285            },
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_split_args_empty() {
296        let p = split_args("");
297        assert!(p.tags.is_empty());
298        assert!(p.attrs.is_empty());
299    }
300
301    #[test]
302    fn test_split_args_tags_only() {
303        let p = split_args("work, release");
304        assert_eq!(p.tags, vec!["work", "release"]);
305        assert!(p.attrs.is_empty());
306    }
307
308    #[test]
309    fn test_split_args_attrs_only() {
310        let p = split_args("status: done");
311        assert!(p.tags.is_empty());
312        assert_eq!(p.attrs.get("status").map(|s| s.as_str()), Some("done"));
313    }
314
315    #[test]
316    fn test_split_args_mixed() {
317        let p = split_args("work, release, status: done");
318        assert_eq!(p.tags, vec!["work", "release"]);
319        assert_eq!(p.attrs.get("status").map(|s| s.as_str()), Some("done"));
320    }
321
322    #[test]
323    fn test_split_args_at_field() {
324        let p = split_args("at: 5pm");
325        assert_eq!(p.attrs.get("at").map(|s| s.as_str()), Some("5pm"));
326    }
327
328    #[test]
329    fn test_element_kind_from_sign() {
330        assert_eq!(ElementKind::from_sign("task"),      ElementKind::Task);
331        assert_eq!(ElementKind::from_sign("note"),      ElementKind::Note);
332        assert_eq!(ElementKind::from_sign("log"),       ElementKind::Log);
333        assert_eq!(ElementKind::from_sign("reminder"),  ElementKind::Reminder);
334        assert_eq!(ElementKind::from_sign("mps"),       ElementKind::MpsGroup);
335        assert_eq!(ElementKind::from_sign("character"), ElementKind::Character);
336        assert_eq!(ElementKind::from_sign("unknown"),   ElementKind::Unknown);
337    }
338}