use core::{fmt, str};
use alloc::collections::{BTreeMap, BTreeSet};
use log::trace;
use thiserror::Error;
use crate::{coroutine::*, flag::types::M2dirFlags, m2dir::types::M2dir, path::M2dirPath};
#[derive(Clone, Debug, Error)]
pub enum M2dirFlagRemoveError {
#[error("M2DIR REMOVE FLAGS failed: unexpected coroutine arg")]
UnexpectedArg,
#[error("M2DIR REMOVE FLAGS failed: missing coroutine arg")]
MissingArg,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct M2dirFlagRemoveOptions {}
pub struct M2dirFlagRemove {
flags_path: M2dirPath,
flags: M2dirFlags,
state: State,
#[allow(dead_code)]
opts: M2dirFlagRemoveOptions,
}
impl M2dirFlagRemove {
pub fn new(
m2dir: &M2dir,
id: impl AsRef<str>,
flags: M2dirFlags,
opts: M2dirFlagRemoveOptions,
) -> Self {
Self {
flags_path: m2dir.flags_path(id.as_ref()),
flags,
state: State::Start,
opts,
}
}
}
impl M2dirCoroutine for M2dirFlagRemove {
type Yield = M2dirYield;
type Return = Result<(), M2dirFlagRemoveError>;
fn resume(&mut self, arg: Option<M2dirArg>) -> M2dirCoroutineState<Self::Yield, Self::Return> {
trace!("remove flags: {}", self.state);
match (&self.state, arg) {
(State::Start, None) => {
trace!("wants existing flags read at {}", self.flags_path);
let paths = BTreeSet::from_iter([self.flags_path.clone()]);
self.state = State::Read;
M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths))
}
(State::Read, Some(M2dirArg::FileRead(contents))) => {
let bytes = contents.into_values().next().unwrap_or_default();
let existing = str::from_utf8(&bytes).unwrap_or("");
let mut remaining = M2dirFlags::from_meta(existing);
remaining.difference(&self.flags);
self.state = State::Done;
if remaining.is_empty() {
trace!("wants flags remove at {}", self.flags_path);
let paths = BTreeSet::from_iter([self.flags_path.clone()]);
M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths))
} else {
trace!(
"wants flags write at {} ({} flags)",
self.flags_path,
remaining.len(),
);
let serialized = remaining.to_meta().into_bytes();
let files = BTreeMap::from_iter([(self.flags_path.clone(), serialized)]);
M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files))
}
}
(State::Done, Some(M2dirArg::FileCreate | M2dirArg::FileRemove)) => {
trace!("flags removed from {}", self.flags_path);
M2dirCoroutineState::Complete(Ok(()))
}
(_, Some(_)) => {
let err = M2dirFlagRemoveError::UnexpectedArg;
M2dirCoroutineState::Complete(Err(err))
}
(_, None) => {
let err = M2dirFlagRemoveError::MissingArg;
M2dirCoroutineState::Complete(Err(err))
}
}
}
}
enum State {
Start,
Read,
Done,
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Start => f.write_str("start"),
Self::Read => f.write_str("read existing flags"),
Self::Done => f.write_str("done"),
}
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use super::*;
#[test]
fn subtracts_flags_and_writes_remainder() {
let m2dir = M2dir::from_path("/tmp/inbox");
let mut to_remove = M2dirFlags::default();
to_remove.insert("$seen");
let mut rm = M2dirFlagRemove::new(
&m2dir,
"entry",
to_remove,
M2dirFlagRemoveOptions::default(),
);
let probes = expect_wants_file_read(&mut rm, None);
let path = probes.into_iter().next().unwrap();
let reply = BTreeMap::from_iter([(path, b"$seen\n$forwarded\n".to_vec())]);
let files = expect_wants_file_create(&mut rm, Some(M2dirArg::FileRead(reply)));
let (_, bytes) = files.into_iter().next().unwrap();
let serialized = str::from_utf8(&bytes).unwrap();
assert!(!serialized.contains("$seen"));
assert!(serialized.contains("$forwarded"));
expect_complete_ok(&mut rm, Some(M2dirArg::FileCreate));
}
#[test]
fn empty_remainder_removes_the_flags_file() {
let m2dir = M2dir::from_path("/tmp/inbox");
let mut to_remove = M2dirFlags::default();
to_remove.insert("$seen");
let mut rm = M2dirFlagRemove::new(
&m2dir,
"entry",
to_remove,
M2dirFlagRemoveOptions::default(),
);
let probes = expect_wants_file_read(&mut rm, None);
let path = probes.into_iter().next().unwrap();
let reply = BTreeMap::from_iter([(path, b"$seen\n".to_vec())]);
let _ = expect_wants_file_remove(&mut rm, Some(M2dirArg::FileRead(reply)));
expect_complete_ok(&mut rm, Some(M2dirArg::FileRemove));
}
#[test]
fn unexpected_arg_at_start_returns_unexpected_arg_error() {
let m2dir = M2dir::from_path("/tmp/inbox");
let mut rm = M2dirFlagRemove::new(
&m2dir,
"entry",
M2dirFlags::default(),
M2dirFlagRemoveOptions::default(),
);
let err = expect_complete_err(&mut rm, Some(M2dirArg::FileCreate));
assert!(matches!(err, M2dirFlagRemoveError::UnexpectedArg));
}
#[test]
fn missing_arg_at_read_returns_missing_arg_error() {
let m2dir = M2dir::from_path("/tmp/inbox");
let mut rm = M2dirFlagRemove::new(
&m2dir,
"entry",
M2dirFlags::default(),
M2dirFlagRemoveOptions::default(),
);
let _ = expect_wants_file_read(&mut rm, None);
let err = expect_complete_err(&mut rm, None);
assert!(matches!(err, M2dirFlagRemoveError::MissingArg));
}
#[test]
fn unexpected_arg_kind_at_done_returns_unexpected_arg_error() {
let m2dir = M2dir::from_path("/tmp/inbox");
let mut to_remove = M2dirFlags::default();
to_remove.insert("$seen");
let mut rm = M2dirFlagRemove::new(
&m2dir,
"entry",
to_remove,
M2dirFlagRemoveOptions::default(),
);
let probes = expect_wants_file_read(&mut rm, None);
let path = probes.into_iter().next().unwrap();
let reply = BTreeMap::from_iter([(path, b"$seen\n".to_vec())]);
let _ = expect_wants_file_remove(&mut rm, Some(M2dirArg::FileRead(reply)));
let err = expect_complete_err(&mut rm, Some(M2dirArg::DirRemove));
assert!(matches!(err, M2dirFlagRemoveError::UnexpectedArg));
}
fn expect_wants_file_read(
cor: &mut M2dirFlagRemove,
arg: Option<M2dirArg>,
) -> BTreeSet<M2dirPath> {
match cor.resume(arg) {
M2dirCoroutineState::Yielded(M2dirYield::WantsFileRead(paths)) => paths,
state => panic!("expected WantsFileRead, got {state:?}"),
}
}
fn expect_wants_file_create(
cor: &mut M2dirFlagRemove,
arg: Option<M2dirArg>,
) -> BTreeMap<M2dirPath, Vec<u8>> {
match cor.resume(arg) {
M2dirCoroutineState::Yielded(M2dirYield::WantsFileCreate(files)) => files,
state => panic!("expected WantsFileCreate, got {state:?}"),
}
}
fn expect_wants_file_remove(
cor: &mut M2dirFlagRemove,
arg: Option<M2dirArg>,
) -> BTreeSet<M2dirPath> {
match cor.resume(arg) {
M2dirCoroutineState::Yielded(M2dirYield::WantsFileRemove(paths)) => paths,
state => panic!("expected WantsFileRemove, got {state:?}"),
}
}
fn expect_complete_ok(cor: &mut M2dirFlagRemove, arg: Option<M2dirArg>) {
match cor.resume(arg) {
M2dirCoroutineState::Complete(Ok(())) => {}
state => panic!("expected Complete(Ok), got {state:?}"),
}
}
fn expect_complete_err(
cor: &mut M2dirFlagRemove,
arg: Option<M2dirArg>,
) -> M2dirFlagRemoveError {
match cor.resume(arg) {
M2dirCoroutineState::Complete(Err(err)) => err,
state => panic!("expected Complete(Err), got {state:?}"),
}
}
}