1use 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#[derive(Clone, Debug, Error)]
59pub enum M2dirCreateError {
60 #[error("M2DIR CREATE failed: unexpected coroutine arg")]
62 UnexpectedArg,
63 #[error("M2DIR CREATE failed: missing coroutine arg")]
65 MissingArg,
66 #[error(transparent)]
68 Resolve(#[from] M2dirStoreError),
69}
70
71#[derive(Clone, Debug, Default, Eq, PartialEq)]
73pub struct M2dirCreateOptions {}
74
75pub struct M2dirCreate {
77 m2dir: M2dir,
78 state: State,
79 #[allow(dead_code)]
80 opts: M2dirCreateOptions,
81}
82
83impl M2dirCreate {
84 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 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}