Skip to main content

io_m2dir/flag/
types.rs

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