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