Skip to main content

io_m2dir/flag/
set.rs

1//! I/O-free coroutine to replace an m2dir entry's flags metadata
2//! file with a new flag set.
3//!
4//! If the new set is empty the metadata file is removed; otherwise
5//! it is overwritten.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use std::fs;
11//!
12//! use io_m2dir::{
13//!     coroutine::{M2dirArg, M2dirCoroutine, M2dirCoroutineState, M2dirYield},
14//!     flag::{
15//!         set::{M2dirFlagSet, M2dirFlagSetOptions},
16//!         types::M2dirFlags,
17//!     },
18//!     m2dir::types::M2dir,
19//! };
20//!
21//! let m2dir = M2dir::from_path("/tmp/inbox");
22//! let opts = M2dirFlagSetOptions::default();
23//! let mut flags = M2dirFlags::default();
24//! flags.insert("$seen");
25//! let mut coroutine = M2dirFlagSet::new(&m2dir, "entry-id", flags, opts);
26//! let mut arg = None;
27//!
28//! loop {
29//!     match coroutine.resume(arg.take()) {
30//!         M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => {
31//!             for (path, bytes) in files {
32//!                 fs::write(path.as_str(), bytes).unwrap();
33//!             }
34//!             arg = Some(M2dirArg::FileCreate);
35//!         }
36//!         M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths)) => {
37//!             for path in paths {
38//!                 let _ = fs::remove_file(path.as_str());
39//!             }
40//!             arg = Some(M2dirArg::FileRemove);
41//!         }
42//!         M2dirCoroutineState::Complete(Ok(())) => break,
43//!         M2dirCoroutineState::Complete(Err(err)) => panic!("{err}"),
44//!         state => panic!("unexpected state {state:?}"),
45//!     }
46//! }
47//! ```
48
49use core::{fmt, str};
50
51use alloc::collections::{BTreeMap, BTreeSet};
52
53use log::trace;
54use thiserror::Error;
55
56use crate::{coroutine::*, flag::types::M2dirFlags, m2dir::types::M2dir, path::M2dirPath};
57
58/// Failure causes during the m2dir flag SET flow.
59#[derive(Clone, Debug, Error)]
60pub enum M2dirFlagSetError {
61    #[error("M2DIR SET FLAGS failed: unexpected coroutine arg")]
62    UnexpectedArg,
63    #[error("M2DIR SET FLAGS failed: missing coroutine arg")]
64    MissingArg,
65}
66
67/// Options for [`M2dirFlagSet::new`].
68#[derive(Clone, Debug, Default, Eq, PartialEq)]
69pub struct M2dirFlagSetOptions {}
70
71/// I/O-free m2dir flag SET coroutine.
72pub struct M2dirFlagSet {
73    flags_path: M2dirPath,
74    flags: M2dirFlags,
75    state: State,
76    #[allow(dead_code)]
77    opts: M2dirFlagSetOptions,
78}
79
80impl M2dirFlagSet {
81    /// Creates a new coroutine that will replace the flags metadata
82    /// file for entry `id` inside `m2dir` with `flags`.
83    pub fn new(
84        m2dir: &M2dir,
85        id: impl AsRef<str>,
86        flags: M2dirFlags,
87        opts: M2dirFlagSetOptions,
88    ) -> Self {
89        Self {
90            flags_path: m2dir.flags_path(id.as_ref()),
91            flags,
92            state: State::Start,
93            opts,
94        }
95    }
96}
97
98impl M2dirCoroutine for M2dirFlagSet {
99    type Yield = M2dirYield;
100    type Return = Result<(), M2dirFlagSetError>;
101
102    fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
103        trace!("set flags: {}", self.state);
104
105        match (&self.state, arg) {
106            (State::Start, None) => {
107                self.state = State::Done;
108
109                if self.flags.is_empty() {
110                    trace!("wants flags remove at {}", self.flags_path);
111                    let paths = BTreeSet::from_iter([self.flags_path.clone()]);
112                    M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths))
113                } else {
114                    trace!(
115                        "wants flags write at {} ({} flags)",
116                        self.flags_path,
117                        self.flags.len(),
118                    );
119                    let serialized = self.flags.to_meta().into_bytes();
120                    let files = BTreeMap::from_iter([(self.flags_path.clone(), serialized)]);
121                    M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files))
122                }
123            }
124            (State::Done, Some(M2dirArg::FileCreate | M2dirArg::FileRemove)) => {
125                trace!("flags set at {}", self.flags_path);
126                M2dirCoroutineState::Complete(Ok(()))
127            }
128            (_, Some(_)) => {
129                let err = M2dirFlagSetError::UnexpectedArg;
130                M2dirCoroutineState::Complete(Err(err))
131            }
132            (_, None) => {
133                let err = M2dirFlagSetError::MissingArg;
134                M2dirCoroutineState::Complete(Err(err))
135            }
136        }
137    }
138}
139
140enum State {
141    Start,
142    Done,
143}
144
145impl fmt::Display for State {
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            Self::Start => f.write_str("start"),
149            Self::Done => f.write_str("done"),
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use alloc::vec::Vec;
157
158    use super::*;
159
160    #[test]
161    fn non_empty_flags_yields_file_create_then_completes_ok() {
162        let m2dir = M2dir::from_path("/tmp/inbox");
163        let mut flags = M2dirFlags::default();
164        flags.insert("$seen");
165
166        let mut set = M2dirFlagSet::new(&m2dir, "entry", flags, M2dirFlagSetOptions::default());
167
168        let files = expect_wants_file_create(&mut set, None);
169        assert_eq!(files.len(), 1);
170        let (path, bytes) = files.into_iter().next().unwrap();
171        assert!(path.as_str().contains("entry.flags"));
172        assert_eq!(str::from_utf8(&bytes).unwrap(), "$seen\n");
173
174        expect_complete_ok(&mut set, Some(M2dirArg::FileCreate));
175    }
176
177    #[test]
178    fn empty_flags_yields_file_remove_then_completes_ok() {
179        let m2dir = M2dir::from_path("/tmp/inbox");
180        let mut set = M2dirFlagSet::new(
181            &m2dir,
182            "entry",
183            M2dirFlags::default(),
184            M2dirFlagSetOptions::default(),
185        );
186
187        let paths = expect_wants_file_remove(&mut set, None);
188        assert_eq!(paths.len(), 1);
189
190        expect_complete_ok(&mut set, Some(M2dirArg::FileRemove));
191    }
192
193    #[test]
194    fn missing_arg_at_done_returns_missing_arg_error() {
195        let m2dir = M2dir::from_path("/tmp/inbox");
196        let mut flags = M2dirFlags::default();
197        flags.insert("$seen");
198
199        let mut set = M2dirFlagSet::new(&m2dir, "entry", flags, M2dirFlagSetOptions::default());
200        let _ = expect_wants_file_create(&mut set, None);
201
202        let err = expect_complete_err(&mut set, None);
203        assert!(matches!(err, M2dirFlagSetError::MissingArg));
204    }
205
206    #[test]
207    fn unexpected_arg_at_start_returns_unexpected_arg_error() {
208        let m2dir = M2dir::from_path("/tmp/inbox");
209        let mut set = M2dirFlagSet::new(
210            &m2dir,
211            "entry",
212            M2dirFlags::default(),
213            M2dirFlagSetOptions::default(),
214        );
215
216        let err = expect_complete_err(&mut set, Some(M2dirArg::FileCreate));
217        assert!(matches!(err, M2dirFlagSetError::UnexpectedArg));
218    }
219
220    #[test]
221    fn unexpected_arg_kind_at_done_returns_unexpected_arg_error() {
222        let m2dir = M2dir::from_path("/tmp/inbox");
223        let mut flags = M2dirFlags::default();
224        flags.insert("$seen");
225
226        let mut set = M2dirFlagSet::new(&m2dir, "entry", flags, M2dirFlagSetOptions::default());
227        let _ = expect_wants_file_create(&mut set, None);
228
229        let err = expect_complete_err(&mut set, Some(M2dirArg::DirRemove));
230        assert!(matches!(err, M2dirFlagSetError::UnexpectedArg));
231    }
232
233    // --- utils
234
235    fn expect_wants_file_create(
236        cor: &mut M2dirFlagSet,
237        arg: Option<M2dirArg>,
238    ) -> BTreeMap<M2dirPath, Vec<u8>> {
239        match cor.resume(arg) {
240            M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => files,
241            state => panic!("expected WantsFileCreate, got {state:?}"),
242        }
243    }
244
245    fn expect_wants_file_remove(
246        cor: &mut M2dirFlagSet,
247        arg: Option<M2dirArg>,
248    ) -> BTreeSet<M2dirPath> {
249        match cor.resume(arg) {
250            M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths)) => paths,
251            state => panic!("expected WantsFileRemove, got {state:?}"),
252        }
253    }
254
255    fn expect_complete_ok(cor: &mut M2dirFlagSet, arg: Option<M2dirArg>) {
256        match cor.resume(arg) {
257            M2dirCoroutineState::Complete(Ok(())) => {}
258            state => panic!("expected Complete(Ok), got {state:?}"),
259        }
260    }
261
262    fn expect_complete_err(cor: &mut M2dirFlagSet, arg: Option<M2dirArg>) -> M2dirFlagSetError {
263        match cor.resume(arg) {
264            M2dirCoroutineState::Complete(Err(err)) => err,
265            state => panic!("expected Complete(Err), got {state:?}"),
266        }
267    }
268}