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