Skip to main content

io_m2dir/m2dir/
delete.rs

1//! I/O-free coroutine to delete an m2dir and every entry it
2//! contains.
3//!
4//! # Example
5//!
6//! ```rust,no_run
7//! use std::fs;
8//!
9//! use io_m2dir::{
10//!     coroutine::{M2dirArg, M2dirCoroutine, M2dirCoroutineState, M2dirYield},
11//!     m2dir::delete::{M2dirDelete, M2dirDeleteOptions},
12//!     path::M2dirPath,
13//! };
14//!
15//! let opts = M2dirDeleteOptions::default();
16//! let path: M2dirPath = "/tmp/inbox".into();
17//! let mut coroutine = M2dirDelete::new(path, opts);
18//! let mut arg = None;
19//!
20//! loop {
21//!     match coroutine.resume(arg.take()) {
22//!         M2dirCoroutineState::Yielded(M2dirYield::WantsDirRemove(paths)) => {
23//!             for path in paths {
24//!                 let _ = fs::remove_dir_all(path.as_str());
25//!             }
26//!             arg = Some(M2dirArg::DirRemove);
27//!         }
28//!         M2dirCoroutineState::Complete(Ok(())) => break,
29//!         M2dirCoroutineState::Complete(Err(err)) => panic!("{err}"),
30//!         state => panic!("unexpected state {state:?}"),
31//!     }
32//! }
33//! ```
34
35use core::fmt;
36
37use alloc::collections::BTreeSet;
38
39use log::trace;
40use thiserror::Error;
41
42use crate::{coroutine::*, path::M2dirPath};
43
44/// Failure causes during the m2dir DELETE flow.
45#[derive(Clone, Debug, Error)]
46pub enum M2dirDeleteError {
47    #[error("M2DIR DELETE failed: unexpected coroutine arg")]
48    UnexpectedArg,
49    #[error("M2DIR DELETE failed: missing coroutine arg")]
50    MissingArg,
51}
52
53/// Options for [`M2dirDelete::new`].
54#[derive(Clone, Debug, Default, Eq, PartialEq)]
55pub struct M2dirDeleteOptions {}
56
57/// I/O-free m2dir DELETE coroutine.
58pub struct M2dirDelete {
59    path: M2dirPath,
60    state: State,
61    #[allow(dead_code)]
62    opts: M2dirDeleteOptions,
63}
64
65impl M2dirDelete {
66    /// Creates a new coroutine that will recursively remove the
67    /// m2dir at `path`.
68    pub fn new(path: impl Into<M2dirPath>, opts: M2dirDeleteOptions) -> Self {
69        Self {
70            path: path.into(),
71            state: State::Start,
72            opts,
73        }
74    }
75}
76
77impl M2dirCoroutine for M2dirDelete {
78    type Yield = M2dirYield;
79    type Return = Result<(), M2dirDeleteError>;
80
81    fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
82        trace!("delete m2dir: {}", self.state);
83
84        match (&self.state, arg) {
85            (State::Start, None) => {
86                let paths = BTreeSet::from_iter([self.path.clone()]);
87                trace!("wants directory removal at {}", self.path);
88                self.state = State::Removed;
89                M2dirCoroutineState::Yielded(M2dirYield::WantsDirRemove(paths))
90            }
91            (State::Removed, Some(M2dirArg::DirRemove)) => {
92                trace!("m2dir removed at {}", self.path);
93                M2dirCoroutineState::Complete(Ok(()))
94            }
95            (_, Some(_)) => {
96                let err = M2dirDeleteError::UnexpectedArg;
97                M2dirCoroutineState::Complete(Err(err))
98            }
99            (_, None) => {
100                let err = M2dirDeleteError::MissingArg;
101                M2dirCoroutineState::Complete(Err(err))
102            }
103        }
104    }
105}
106
107enum State {
108    Start,
109    Removed,
110}
111
112impl fmt::Display for State {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        match self {
115            Self::Start => f.write_str("start"),
116            Self::Removed => f.write_str("removed"),
117        }
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn success_returns_ok() {
127        let mut delete = M2dirDelete::new("/tmp/inbox", M2dirDeleteOptions::default());
128
129        let paths = expect_wants_dir_remove(&mut delete, None);
130        assert_eq!(paths.len(), 1);
131        assert!(paths.contains(&M2dirPath::from("/tmp/inbox")));
132
133        expect_complete_ok(&mut delete, Some(M2dirArg::DirRemove));
134    }
135
136    #[test]
137    fn missing_arg_at_removed_state_returns_missing_arg_error() {
138        let mut delete = M2dirDelete::new("/tmp/inbox", M2dirDeleteOptions::default());
139        let _ = expect_wants_dir_remove(&mut delete, None);
140
141        let err = expect_complete_err(&mut delete, None);
142        assert!(matches!(err, M2dirDeleteError::MissingArg));
143    }
144
145    #[test]
146    fn unexpected_arg_at_start_returns_unexpected_arg_error() {
147        let mut delete = M2dirDelete::new("/tmp/inbox", M2dirDeleteOptions::default());
148
149        let err = expect_complete_err(&mut delete, Some(M2dirArg::DirRemove));
150        assert!(matches!(err, M2dirDeleteError::UnexpectedArg));
151    }
152
153    #[test]
154    fn unexpected_arg_kind_at_removed_state_returns_unexpected_arg_error() {
155        let mut delete = M2dirDelete::new("/tmp/inbox", M2dirDeleteOptions::default());
156        let _ = expect_wants_dir_remove(&mut delete, None);
157
158        let err = expect_complete_err(&mut delete, Some(M2dirArg::FileCreate));
159        assert!(matches!(err, M2dirDeleteError::UnexpectedArg));
160    }
161
162    #[test]
163    fn first_yield_carries_the_target_path() {
164        let mut delete = M2dirDelete::new("/tmp/some/deep/inbox", M2dirDeleteOptions::default());
165
166        let paths = expect_wants_dir_remove(&mut delete, None);
167        assert_eq!(
168            paths.iter().next().map(M2dirPath::as_str),
169            Some("/tmp/some/deep/inbox")
170        );
171    }
172
173    // --- utils
174
175    fn expect_wants_dir_remove(
176        cor: &mut M2dirDelete,
177        arg: Option<M2dirArg>,
178    ) -> BTreeSet<M2dirPath> {
179        match cor.resume(arg) {
180            M2dirCoroutineState::Yielded(M2dirYield::WantsDirRemove(paths)) => paths,
181            state => panic!("expected WantsDirRemove, got {state:?}"),
182        }
183    }
184
185    fn expect_complete_ok(cor: &mut M2dirDelete, arg: Option<M2dirArg>) {
186        match cor.resume(arg) {
187            M2dirCoroutineState::Complete(Ok(())) => {}
188            state => panic!("expected Complete(Ok), got {state:?}"),
189        }
190    }
191
192    fn expect_complete_err(cor: &mut M2dirDelete, arg: Option<M2dirArg>) -> M2dirDeleteError {
193        match cor.resume(arg) {
194            M2dirCoroutineState::Complete(Err(err)) => err,
195            state => panic!("expected Complete(Err), got {state:?}"),
196        }
197    }
198}