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