Skip to main content

io_m2dir/m2dir/
create.rs

1//! I/O-free coroutine to create an m2dir: the folder, its
2//! `.meta` sub-directory and the `.m2dir` marker file.
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::create::{M2dirCreate, M2dirCreateOptions},
12//!     store::M2dirStore,
13//! };
14//!
15//! let store = M2dirStore::from_path("/tmp/store");
16//! let opts = M2dirCreateOptions::default();
17//! let mut coroutine = M2dirCreate::new(&store, "inbox", opts).unwrap();
18//! let mut arg = None;
19//!
20//! loop {
21//!     match coroutine.resume(arg.take()) {
22//!         M2dirCoroutineState::Yielded(M2dirYield::WantsDirCreate(paths)) => {
23//!             for path in paths {
24//!                 fs::create_dir_all(path.as_str()).unwrap();
25//!             }
26//!             arg = Some(M2dirArg::DirCreate);
27//!         }
28//!         M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => {
29//!             for (path, bytes) in files {
30//!                 fs::write(path.as_str(), bytes).unwrap();
31//!             }
32//!             arg = Some(M2dirArg::FileCreate);
33//!         }
34//!         M2dirCoroutineState::Complete(Ok(_)) => break,
35//!         M2dirCoroutineState::Complete(Err(err)) => panic!("{err}"),
36//!         state => panic!("unexpected state {state:?}"),
37//!     }
38//! }
39//! ```
40
41use core::fmt;
42
43use alloc::{
44    collections::{BTreeMap, BTreeSet},
45    vec::Vec,
46};
47
48use log::trace;
49use thiserror::Error;
50
51use crate::{
52    coroutine::*,
53    m2dir::M2dir,
54    store::{M2dirStore, M2dirStoreError},
55};
56
57/// Failure causes during the m2dir CREATE flow.
58#[derive(Clone, Debug, Error)]
59pub enum M2dirCreateError {
60    /// The resume arg did not match the pending filesystem request.
61    #[error("M2DIR CREATE failed: unexpected coroutine arg")]
62    UnexpectedArg,
63    /// The coroutine was resumed without the arg its request needs.
64    #[error("M2DIR CREATE failed: missing coroutine arg")]
65    MissingArg,
66    /// Resolving the folder name against the store failed.
67    #[error(transparent)]
68    Resolve(#[from] M2dirStoreError),
69}
70
71/// Options for [`M2dirCreate::new`].
72#[derive(Clone, Debug, Default, Eq, PartialEq)]
73pub struct M2dirCreateOptions {}
74
75/// I/O-free m2dir CREATE coroutine.
76pub struct M2dirCreate {
77    m2dir: M2dir,
78    state: State,
79    #[allow(dead_code)]
80    opts: M2dirCreateOptions,
81}
82
83impl M2dirCreate {
84    /// Creates a new coroutine that will create the folder named
85    /// `name` inside `store`.
86    pub fn new(
87        store: &M2dirStore,
88        name: &str,
89        opts: M2dirCreateOptions,
90    ) -> Result<Self, M2dirStoreError> {
91        let path = store.resolve_folder_path(name)?;
92        let m2dir = M2dir::from_path(path);
93
94        Ok(Self {
95            m2dir,
96            state: State::Start,
97            opts,
98        })
99    }
100
101    /// Returns the [`M2dir`] this coroutine targets.
102    pub fn m2dir(&self) -> &M2dir {
103        &self.m2dir
104    }
105}
106
107impl M2dirCoroutine for M2dirCreate {
108    type Yield = M2dirYield;
109    type Return = Result<M2dir, M2dirCreateError>;
110
111    fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
112        match (&self.state, arg) {
113            (State::Start, None) => {
114                trace!("wants directory creation for {}", self.m2dir.path());
115
116                let root = self.m2dir.path().clone();
117                let meta = self.m2dir.meta_dir();
118                let paths = BTreeSet::from_iter([root, meta]);
119
120                self.state = State::DirCreated;
121                M2dirCoroutineState::Yielded(M2dirYield::WantsDirCreate(paths))
122            }
123            (State::DirCreated, Some(M2dirArg::DirCreate)) => {
124                trace!("wants marker file at {}", self.m2dir.marker_path());
125
126                let marker = self.m2dir.marker_path();
127                let files = BTreeMap::from_iter([(marker, Vec::new())]);
128
129                self.state = State::MarkerWritten;
130                M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files))
131            }
132            (State::MarkerWritten, Some(M2dirArg::FileCreate)) => {
133                trace!("m2dir created at {}", self.m2dir.path());
134                M2dirCoroutineState::Complete(Ok(self.m2dir.clone()))
135            }
136            (_, Some(_)) => {
137                let err = M2dirCreateError::UnexpectedArg;
138                M2dirCoroutineState::Complete(Err(err))
139            }
140            (_, None) => {
141                let err = M2dirCreateError::MissingArg;
142                M2dirCoroutineState::Complete(Err(err))
143            }
144        }
145    }
146}
147
148enum State {
149    Start,
150    DirCreated,
151    MarkerWritten,
152}
153
154impl fmt::Display for State {
155    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
156        match self {
157            Self::Start => f.write_str("start"),
158            Self::DirCreated => f.write_str("directory created"),
159            Self::MarkerWritten => f.write_str("marker written"),
160        }
161    }
162}
163
164#[cfg(test)]
165mod tests {
166    use crate::path::M2dirPath;
167
168    use super::*;
169
170    #[test]
171    fn success_returns_the_new_m2dir() {
172        let store = M2dirStore::from_path("/tmp/store");
173        let mut create =
174            M2dirCreate::new(&store, "inbox", M2dirCreateOptions::default()).expect("valid name");
175
176        let paths = expect_wants_dir_create(&mut create, None);
177        assert!(paths.iter().any(|p| p.as_str() == "/tmp/store/inbox"));
178        assert!(paths.iter().any(|p| p.as_str() == "/tmp/store/inbox/.meta"));
179
180        let files = expect_wants_file_create(&mut create, Some(M2dirArg::DirCreate));
181        assert!(
182            files
183                .keys()
184                .any(|p| p.as_str() == "/tmp/store/inbox/.m2dir")
185        );
186
187        let m2dir = match create.resume(Some(M2dirArg::FileCreate)) {
188            M2dirCoroutineState::Complete(Ok(m2dir)) => m2dir,
189            state => panic!("expected Complete(Ok), got {state:?}"),
190        };
191        assert_eq!(m2dir.path().as_str(), "/tmp/store/inbox");
192    }
193
194    #[test]
195    fn escaping_name_returns_resolve_error_at_construction() {
196        let store = M2dirStore::from_path("/tmp/store");
197        let result = M2dirCreate::new(&store, "../escape", M2dirCreateOptions::default());
198        assert!(matches!(result, Err(M2dirStoreError::EscapesRoot(_))));
199    }
200
201    #[test]
202    fn unexpected_arg_at_start_returns_unexpected_arg_error() {
203        let store = M2dirStore::from_path("/tmp/store");
204        let mut create = M2dirCreate::new(&store, "inbox", M2dirCreateOptions::default()).unwrap();
205
206        let err = expect_complete_err(&mut create, Some(M2dirArg::DirCreate));
207        assert!(matches!(err, M2dirCreateError::UnexpectedArg));
208    }
209
210    #[test]
211    fn missing_arg_at_dir_created_returns_missing_arg_error() {
212        let store = M2dirStore::from_path("/tmp/store");
213        let mut create = M2dirCreate::new(&store, "inbox", M2dirCreateOptions::default()).unwrap();
214        let _ = expect_wants_dir_create(&mut create, None);
215
216        let err = expect_complete_err(&mut create, None);
217        assert!(matches!(err, M2dirCreateError::MissingArg));
218    }
219
220    #[test]
221    fn unexpected_arg_kind_at_marker_written_returns_unexpected_arg_error() {
222        let store = M2dirStore::from_path("/tmp/store");
223        let mut create = M2dirCreate::new(&store, "inbox", M2dirCreateOptions::default()).unwrap();
224        let _ = expect_wants_dir_create(&mut create, None);
225        let _ = expect_wants_file_create(&mut create, Some(M2dirArg::DirCreate));
226
227        let err = expect_complete_err(&mut create, Some(M2dirArg::DirCreate));
228        assert!(matches!(err, M2dirCreateError::UnexpectedArg));
229    }
230
231    fn expect_wants_dir_create(
232        cor: &mut M2dirCreate,
233        arg: Option<M2dirArg>,
234    ) -> BTreeSet<M2dirPath> {
235        match cor.resume(arg) {
236            M2dirCoroutineState::Yielded(M2dirYield::WantsDirCreate(paths)) => paths,
237            state => panic!("expected WantsDirCreate, got {state:?}"),
238        }
239    }
240
241    fn expect_wants_file_create(
242        cor: &mut M2dirCreate,
243        arg: Option<M2dirArg>,
244    ) -> BTreeMap<M2dirPath, Vec<u8>> {
245        match cor.resume(arg) {
246            M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => files,
247            state => panic!("expected WantsFileCreate, got {state:?}"),
248        }
249    }
250
251    fn expect_complete_err(cor: &mut M2dirCreate, arg: Option<M2dirArg>) -> M2dirCreateError {
252        match cor.resume(arg) {
253            M2dirCoroutineState::Complete(Err(err)) => err,
254            state => panic!("expected Complete(Err), got {state:?}"),
255        }
256    }
257}