Skip to main content

io_m2dir/
flag.rs

1//! Flag-level m2dir coroutines and their shared type.
2//!
3//! Flags are stored per entry in a `.meta/<id>.flags` sidecar, one flag
4//! per line. This module holds the [`M2dirFlags`] set, shared by the
5//! add, remove and set coroutines below.
6
7pub mod add;
8pub mod remove;
9pub mod set;
10
11use core::fmt;
12
13use alloc::{
14    collections::BTreeSet,
15    string::{String, ToString},
16    vec::Vec,
17};
18
19/// Set of flags attached to an m2dir entry.
20///
21/// Each flag is an arbitrary UTF-8 string; serialization to the
22/// `.meta/<id>.flags` metadata file is one flag per line.
23#[derive(Clone, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
24pub struct M2dirFlags(BTreeSet<String>);
25
26impl M2dirFlags {
27    /// Returns an iterator over the flags in this set.
28    pub fn iter(&self) -> impl Iterator<Item = &str> {
29        self.0.iter().map(String::as_str)
30    }
31
32    /// Returns the number of flags in this set.
33    pub fn len(&self) -> usize {
34        self.0.len()
35    }
36
37    /// Returns `true` if the set contains no flags.
38    pub fn is_empty(&self) -> bool {
39        self.0.is_empty()
40    }
41
42    /// Inserts a flag into the set. Returns `true` if it was not
43    /// already present.
44    pub fn insert(&mut self, flag: impl Into<String>) -> bool {
45        self.0.insert(flag.into())
46    }
47
48    /// Removes a flag from the set. Returns `true` if it was present.
49    pub fn remove(&mut self, flag: &str) -> bool {
50        self.0.remove(flag)
51    }
52
53    /// Returns `true` if the set contains the given flag.
54    pub fn contains(&self, flag: &str) -> bool {
55        self.0.contains(flag)
56    }
57
58    /// Adds every flag from `flags` to this set.
59    pub fn extend(&mut self, flags: M2dirFlags) {
60        self.0.extend(flags.0);
61    }
62
63    /// Removes every flag in `flags` from this set.
64    pub fn difference(&mut self, flags: &M2dirFlags) {
65        self.0 = self.0.difference(&flags.0).cloned().collect();
66    }
67
68    /// Serializes the flag set to its on-disk `.flags` representation:
69    /// one flag per line, deterministic alphabetical order.
70    pub fn to_meta(&self) -> String {
71        let mut out = String::new();
72        for flag in &self.0 {
73            out.push_str(flag);
74            out.push('\n');
75        }
76        out
77    }
78
79    /// Parses a `.flags` metadata payload (one flag per line, blanks
80    /// ignored).
81    pub fn from_meta(contents: &str) -> Self {
82        Self(
83            contents
84                .lines()
85                .filter(|line| !line.is_empty())
86                .map(ToString::to_string)
87                .collect(),
88        )
89    }
90}
91
92impl fmt::Display for M2dirFlags {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        let sorted: Vec<&str> = self.0.iter().map(String::as_str).collect();
95        write!(f, "{}", sorted.join(","))
96    }
97}
98
99impl FromIterator<String> for M2dirFlags {
100    fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Self {
101        Self(iter.into_iter().collect())
102    }
103}
104
105impl<'a> FromIterator<&'a str> for M2dirFlags {
106    fn from_iter<I: IntoIterator<Item = &'a str>>(iter: I) -> Self {
107        Self(iter.into_iter().map(ToString::to_string).collect())
108    }
109}
110
111impl From<BTreeSet<String>> for M2dirFlags {
112    fn from(set: BTreeSet<String>) -> Self {
113        Self(set)
114    }
115}
116
117impl From<M2dirFlags> for BTreeSet<String> {
118    fn from(flags: M2dirFlags) -> Self {
119        flags.0
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use crate::flag::M2dirFlags;
126
127    #[test]
128    fn meta_round_trip() {
129        let mut flags = M2dirFlags::default();
130        flags.insert("$seen");
131        flags.insert("$forwarded");
132        flags.insert("custom");
133
134        let serialized = flags.to_meta();
135        let parsed = M2dirFlags::from_meta(&serialized);
136
137        assert_eq!(parsed.len(), 3);
138        assert!(parsed.contains("$seen"));
139        assert!(parsed.contains("$forwarded"));
140        assert!(parsed.contains("custom"));
141    }
142
143    #[test]
144    fn meta_is_sorted() {
145        let mut flags = M2dirFlags::default();
146        flags.insert("zeta");
147        flags.insert("alpha");
148        flags.insert("middle");
149        assert_eq!(flags.to_meta(), "alpha\nmiddle\nzeta\n");
150    }
151
152    #[test]
153    fn from_meta_ignores_blanks() {
154        let parsed = M2dirFlags::from_meta("$seen\n\n\n$forwarded\n");
155        assert_eq!(parsed.len(), 2);
156        assert!(parsed.contains("$seen"));
157        assert!(parsed.contains("$forwarded"));
158    }
159}