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    /// The resume arg did not match the pending filesystem request.
48    #[error("M2DIR DELETE failed: unexpected coroutine arg")]
49    UnexpectedArg,
50    /// The coroutine was resumed without the arg its request needs.
51    #[error("M2DIR DELETE failed: missing coroutine arg")]
52    MissingArg,
53}
54
55/// Options for [`M2dirDelete::new`].
56#[derive(Clone, Debug, Default, Eq, PartialEq)]
57pub struct M2dirDeleteOptions {}
58
59/// I/O-free m2dir DELETE coroutine.
60pub struct M2dirDelete {
61    path: M2dirPath,
62    state: State,
63    #[allow(dead_code)]
64    opts: M2dirDeleteOptions,
65}
66
67impl M2dirDelete {
68    /// Creates a new coroutine that will recursively remove the
69    /// m2dir at `path`.
70    pub fn new(path: impl Into<M2dirPath>, opts: M2dirDeleteOptions) -> Self {
71        Self {
72            path: path.into(),
73            state: State::Start,
74            opts,
75        }
76    }
77}
78
79impl M2dirCoroutine for M2dirDelete {
80    type Yield = M2dirYield;
81    type Return = Result<(), M2dirDeleteError>;
82
83    fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
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    fn expect_wants_dir_remove(
174        cor: &mut M2dirDelete,
175        arg: Option<M2dirArg>,
176    ) -> BTreeSet<M2dirPath> {
177        match cor.resume(arg) {
178            M2dirCoroutineState::Yielded(M2dirYield::WantsDirRemove(paths)) => paths,
179            state => panic!("expected WantsDirRemove, got {state:?}"),
180        }
181    }
182
183    fn expect_complete_ok(cor: &mut M2dirDelete, arg: Option<M2dirArg>) {
184        match cor.resume(arg) {
185            M2dirCoroutineState::Complete(Ok(())) => {}
186            state => panic!("expected Complete(Ok), got {state:?}"),
187        }
188    }
189
190    fn expect_complete_err(cor: &mut M2dirDelete, arg: Option<M2dirArg>) -> M2dirDeleteError {
191        match cor.resume(arg) {
192            M2dirCoroutineState::Complete(Err(err)) => err,
193            state => panic!("expected Complete(Err), got {state:?}"),
194        }
195    }
196}