1use core::{fmt, str};
52
53use alloc::collections::{BTreeMap, BTreeSet};
54
55use log::trace;
56use thiserror::Error;
57
58use crate::{coroutine::*, flag::types::M2dirFlags, m2dir::types::M2dir, path::M2dirPath};
59
60#[derive(Clone, Debug, Error)]
62pub enum M2dirFlagAddError {
63 #[error("M2DIR ADD FLAGS failed: unexpected coroutine arg")]
64 UnexpectedArg,
65 #[error("M2DIR ADD FLAGS failed: missing coroutine arg")]
66 MissingArg,
67}
68
69#[derive(Clone, Debug, Default, Eq, PartialEq)]
71pub struct M2dirFlagAddOptions {}
72
73pub struct M2dirFlagAdd {
75 flags_path: M2dirPath,
76 flags: M2dirFlags,
77 state: State,
78 #[allow(dead_code)]
79 opts: M2dirFlagAddOptions,
80}
81
82impl M2dirFlagAdd {
83 pub fn new(
86 m2dir: &M2dir,
87 id: impl AsRef<str>,
88 flags: M2dirFlags,
89 opts: M2dirFlagAddOptions,
90 ) -> Self {
91 Self {
92 flags_path: m2dir.flags_path(id.as_ref()),
93 flags,
94 state: State::Start,
95 opts,
96 }
97 }
98}
99
100impl M2dirCoroutine for M2dirFlagAdd {
101 type Yield = M2dirYield;
102 type Return = Result<(), M2dirFlagAddError>;
103
104 fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
105 trace!("add flags: {}", self.state);
106
107 match (&self.state, arg) {
108 (State::Start, None) => {
109 trace!("wants existing flags read at {}", self.flags_path);
110 let paths = BTreeSet::from_iter([self.flags_path.clone()]);
111 self.state = State::Read;
112 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths))
113 }
114 (State::Read, Some(M2dirArg::FileRead(contents))) => {
115 let bytes = contents.into_values().next().unwrap_or_default();
116 let existing = str::from_utf8(&bytes).unwrap_or("");
117
118 let mut merged = M2dirFlags::from_meta(existing);
119 merged.extend(self.flags.clone());
120
121 trace!(
122 "wants flags write at {} ({} flags)",
123 self.flags_path,
124 merged.len(),
125 );
126
127 let serialized = merged.to_meta().into_bytes();
128 let files = BTreeMap::from_iter([(self.flags_path.clone(), serialized)]);
129
130 self.state = State::Written;
131 M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files))
132 }
133 (State::Written, Some(M2dirArg::FileCreate)) => {
134 trace!("flags added to {}", self.flags_path);
135 M2dirCoroutineState::Complete(Ok(()))
136 }
137 (_, Some(_)) => {
138 let err = M2dirFlagAddError::UnexpectedArg;
139 M2dirCoroutineState::Complete(Err(err))
140 }
141 (_, None) => {
142 let err = M2dirFlagAddError::MissingArg;
143 M2dirCoroutineState::Complete(Err(err))
144 }
145 }
146 }
147}
148
149enum State {
150 Start,
151 Read,
152 Written,
153}
154
155impl fmt::Display for State {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 match self {
158 Self::Start => f.write_str("start"),
159 Self::Read => f.write_str("read existing flags"),
160 Self::Written => f.write_str("written merged flags"),
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use alloc::vec::Vec;
168
169 use super::*;
170
171 #[test]
172 fn merges_with_existing_flags() {
173 let m2dir = M2dir::from_path("/tmp/inbox");
174 let mut flags = M2dirFlags::default();
175 flags.insert("$forwarded");
176
177 let mut add = M2dirFlagAdd::new(&m2dir, "entry", flags, M2dirFlagAddOptions::default());
178
179 let probes = expect_wants_file_read(&mut add, None);
180 let existing_path = probes.into_iter().next().unwrap();
181 let mut reply = BTreeMap::new();
182 reply.insert(existing_path, b"$seen\n".to_vec());
183
184 let files = expect_wants_file_create(&mut add, Some(M2dirArg::FileRead(reply)));
185 let (_, bytes) = files.into_iter().next().unwrap();
186 let serialized = str::from_utf8(&bytes).unwrap();
187 assert!(serialized.contains("$seen"));
188 assert!(serialized.contains("$forwarded"));
189
190 expect_complete_ok(&mut add, Some(M2dirArg::FileCreate));
191 }
192
193 #[test]
194 fn empty_existing_writes_only_the_new_flags() {
195 let m2dir = M2dir::from_path("/tmp/inbox");
196 let mut flags = M2dirFlags::default();
197 flags.insert("$seen");
198
199 let mut add = M2dirFlagAdd::new(&m2dir, "entry", flags, M2dirFlagAddOptions::default());
200
201 let probes = expect_wants_file_read(&mut add, None);
202 let path = probes.into_iter().next().unwrap();
203 let reply = BTreeMap::from_iter([(path, Vec::<u8>::new())]);
204
205 let files = expect_wants_file_create(&mut add, Some(M2dirArg::FileRead(reply)));
206 let (_, bytes) = files.into_iter().next().unwrap();
207 assert_eq!(str::from_utf8(&bytes).unwrap(), "$seen\n");
208
209 expect_complete_ok(&mut add, Some(M2dirArg::FileCreate));
210 }
211
212 #[test]
213 fn unexpected_arg_at_start_returns_unexpected_arg_error() {
214 let m2dir = M2dir::from_path("/tmp/inbox");
215 let mut add = M2dirFlagAdd::new(
216 &m2dir,
217 "entry",
218 M2dirFlags::default(),
219 M2dirFlagAddOptions::default(),
220 );
221
222 let err = expect_complete_err(&mut add, Some(M2dirArg::FileCreate));
223 assert!(matches!(err, M2dirFlagAddError::UnexpectedArg));
224 }
225
226 #[test]
227 fn missing_arg_at_read_returns_missing_arg_error() {
228 let m2dir = M2dir::from_path("/tmp/inbox");
229 let mut add = M2dirFlagAdd::new(
230 &m2dir,
231 "entry",
232 M2dirFlags::default(),
233 M2dirFlagAddOptions::default(),
234 );
235 let _ = expect_wants_file_read(&mut add, None);
236
237 let err = expect_complete_err(&mut add, None);
238 assert!(matches!(err, M2dirFlagAddError::MissingArg));
239 }
240
241 #[test]
242 fn unexpected_arg_kind_at_written_returns_unexpected_arg_error() {
243 let m2dir = M2dir::from_path("/tmp/inbox");
244 let mut flags = M2dirFlags::default();
245 flags.insert("$seen");
246
247 let mut add = M2dirFlagAdd::new(&m2dir, "entry", flags, M2dirFlagAddOptions::default());
248
249 let probes = expect_wants_file_read(&mut add, None);
250 let path = probes.into_iter().next().unwrap();
251 let reply = BTreeMap::from_iter([(path, Vec::<u8>::new())]);
252 let _ = expect_wants_file_create(&mut add, Some(M2dirArg::FileRead(reply)));
253
254 let err = expect_complete_err(&mut add, Some(M2dirArg::DirRemove));
255 assert!(matches!(err, M2dirFlagAddError::UnexpectedArg));
256 }
257
258 fn expect_wants_file_read(
261 cor: &mut M2dirFlagAdd,
262 arg: Option<M2dirArg>,
263 ) -> BTreeSet<M2dirPath> {
264 match cor.resume(arg) {
265 M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths)) => paths,
266 state => panic!("expected WantsFileRead, got {state:?}"),
267 }
268 }
269
270 fn expect_wants_file_create(
271 cor: &mut M2dirFlagAdd,
272 arg: Option<M2dirArg>,
273 ) -> BTreeMap<M2dirPath, Vec<u8>> {
274 match cor.resume(arg) {
275 M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => files,
276 state => panic!("expected WantsFileCreate, got {state:?}"),
277 }
278 }
279
280 fn expect_complete_ok(cor: &mut M2dirFlagAdd, arg: Option<M2dirArg>) {
281 match cor.resume(arg) {
282 M2dirCoroutineState::Complete(Ok(())) => {}
283 state => panic!("expected Complete(Ok), got {state:?}"),
284 }
285 }
286
287 fn expect_complete_err(cor: &mut M2dirFlagAdd, arg: Option<M2dirArg>) -> M2dirFlagAddError {
288 match cor.resume(arg) {
289 M2dirCoroutineState::Complete(Err(err)) => err,
290 state => panic!("expected Complete(Err), got {state:?}"),
291 }
292 }
293}