use core::{fmt, mem};
use alloc::{
collections::{BTreeMap, BTreeSet},
format,
string::String,
vec::Vec,
};
use thiserror::Error;
use crate::{
collection::{COLOR, DESCRIPTION, DISPLAYNAME, VdirCollection},
coroutine::*,
item::TMP,
path::VdirPath,
};
#[derive(Clone, Debug, Error)]
pub enum VdirCollectionUpdateError {
#[error("Vdir collection update failed: unexpected arg {0:?}")]
UnexpectedArg(Option<VdirReply>),
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct VdirCollectionUpdateOptions {}
#[derive(Debug)]
pub struct VdirCollectionUpdate {
state: State,
#[allow(dead_code)]
opts: VdirCollectionUpdateOptions,
}
impl VdirCollectionUpdate {
pub fn new(collection: VdirCollection, opts: VdirCollectionUpdateOptions) -> Self {
Self {
opts,
state: State::Start(collection),
}
}
}
impl VdirCoroutine for VdirCollectionUpdate {
type Yield = VdirYield;
type Return = Result<(), VdirCollectionUpdateError>;
fn resume(&mut self, arg: Option<VdirReply>) -> VdirCoroutineState<Self::Yield, Self::Return> {
match (&mut self.state, arg) {
(State::Start(collection), None) => {
let collection = mem::take(collection);
let mut files = BTreeMap::new();
let mut renames = Vec::new();
let mut removals = BTreeSet::new();
let mut field = |name: &str, value: Option<String>| {
let final_path = collection.path.join(name);
match value.filter(|value| !value.is_empty()) {
Some(value) => {
let tmp_path = final_path.with_file_name(&format!("{name}.{TMP}"));
files.insert(tmp_path.clone(), value.into_bytes());
renames.push((tmp_path, final_path));
}
None => {
removals.insert(final_path);
}
}
};
field(DISPLAYNAME, collection.display_name.clone());
field(DESCRIPTION, collection.description.clone());
field(COLOR, collection.color.clone());
if files.is_empty() {
self.state = State::RemoveFiles;
return VdirCoroutineState::Yielded(VdirYield::WantsFileRemove(removals));
}
self.state = State::CreateFile { renames, removals };
VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files))
}
(State::CreateFile { renames, removals }, Some(VdirReply::FileCreate)) => {
let renames = mem::take(renames);
let removals = mem::take(removals);
self.state = State::Rename { removals };
VdirCoroutineState::Yielded(VdirYield::WantsRename(renames))
}
(State::Rename { removals }, Some(VdirReply::Rename)) => {
let removals = mem::take(removals);
if removals.is_empty() {
return VdirCoroutineState::Complete(Ok(()));
}
self.state = State::RemoveFiles;
VdirCoroutineState::Yielded(VdirYield::WantsFileRemove(removals))
}
(State::RemoveFiles, Some(VdirReply::FileRemove)) => {
VdirCoroutineState::Complete(Ok(()))
}
(_, arg) => {
let err = VdirCollectionUpdateError::UnexpectedArg(arg);
VdirCoroutineState::Complete(Err(err))
}
}
}
}
#[derive(Debug)]
enum State {
Start(VdirCollection),
CreateFile {
renames: Vec<(VdirPath, VdirPath)>,
removals: BTreeSet<VdirPath>,
},
Rename {
removals: BTreeSet<VdirPath>,
},
RemoveFiles,
}
impl fmt::Display for State {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Start(_) => f.write_str("start"),
Self::CreateFile { .. } => f.write_str("write metadata into tmp"),
Self::Rename { .. } => f.write_str("rename into place"),
Self::RemoveFiles => f.write_str("remove cleared metadata"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn writes_temp_files_then_renames() {
let collection = VdirCollection {
path: VdirPath::from("root/contacts"),
display_name: Some("Contacts".into()),
description: None,
color: None,
};
let mut cor = VdirCollectionUpdate::new(collection, VdirCollectionUpdateOptions::default());
let files = match cor.resume(None) {
VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files)) => files,
state => panic!("expected WantsFileCreate, got {state:?}"),
};
let tmp = VdirPath::from("root/contacts/displayname.tmp");
assert!(files.contains_key(&tmp));
let pairs = match cor.resume(Some(VdirReply::FileCreate)) {
VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => pairs,
state => panic!("expected WantsRename, got {state:?}"),
};
assert_eq!(
pairs,
vec![(tmp, VdirPath::from("root/contacts/displayname"))]
);
let removals = match cor.resume(Some(VdirReply::Rename)) {
VdirCoroutineState::Yielded(VdirYield::WantsFileRemove(removals)) => removals,
state => panic!("expected WantsFileRemove, got {state:?}"),
};
assert!(removals.contains(&VdirPath::from("root/contacts/description")));
assert!(removals.contains(&VdirPath::from("root/contacts/color")));
assert!(!removals.contains(&VdirPath::from("root/contacts/displayname")));
match cor.resume(Some(VdirReply::FileRemove)) {
VdirCoroutineState::Complete(Ok(())) => {}
state => panic!("expected Complete(Ok), got {state:?}"),
}
}
#[test]
fn no_metadata_removes_every_file() {
let mut cor = VdirCollectionUpdate::new(
VdirCollection::from_path("root/contacts"),
VdirCollectionUpdateOptions::default(),
);
let removals = match cor.resume(None) {
VdirCoroutineState::Yielded(VdirYield::WantsFileRemove(removals)) => removals,
state => panic!("expected WantsFileRemove, got {state:?}"),
};
assert_eq!(removals.len(), 3);
match cor.resume(Some(VdirReply::FileRemove)) {
VdirCoroutineState::Complete(Ok(())) => {}
state => panic!("expected Complete(Ok), got {state:?}"),
}
}
#[test]
fn unexpected_reply_returns_error() {
let collection = VdirCollection {
path: VdirPath::from("root/contacts"),
display_name: Some("Contacts".into()),
description: None,
color: None,
};
let mut cor = VdirCollectionUpdate::new(collection, VdirCollectionUpdateOptions::default());
let _ = cor.resume(None);
let err = match cor.resume(Some(VdirReply::DirExists(BTreeMap::new()))) {
VdirCoroutineState::Complete(Err(err)) => err,
state => panic!("expected Complete(Err), got {state:?}"),
};
assert!(matches!(err, VdirCollectionUpdateError::UnexpectedArg(_)));
}
}