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::types::M2dir,
54 store::{M2dirStore, M2dirStoreError},
55};
56
57#[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#[derive(Clone, Debug, Default, Eq, PartialEq)]
70pub struct M2dirCreateOptions {}
71
72pub struct M2dirCreate {
74 m2dir: M2dir,
75 state: State,
76 #[allow(dead_code)]
77 opts: M2dirCreateOptions,
78}
79
80impl M2dirCreate {
81 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 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 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}