1use core::{fmt, mem};
45
46use alloc::{
47 collections::{BTreeMap, BTreeSet},
48 format,
49 string::String,
50 vec::Vec,
51};
52
53use thiserror::Error;
54
55use crate::{
56 collection::{COLOR, DESCRIPTION, DISPLAYNAME, VdirCollection},
57 coroutine::*,
58 item::TMP,
59 path::VdirPath,
60};
61
62#[derive(Clone, Debug, Error)]
64pub enum VdirCollectionUpdateError {
65 #[error("Vdir collection update failed: unexpected arg {0:?}")]
68 UnexpectedArg(Option<VdirReply>),
69}
70
71#[derive(Clone, Debug, Default, Eq, PartialEq)]
73pub struct VdirCollectionUpdateOptions {}
74
75#[derive(Debug)]
77pub struct VdirCollectionUpdate {
78 state: State,
79 #[allow(dead_code)]
80 opts: VdirCollectionUpdateOptions,
81}
82
83impl VdirCollectionUpdate {
84 pub fn new(collection: VdirCollection, opts: VdirCollectionUpdateOptions) -> Self {
87 Self {
88 opts,
89 state: State::Start(collection),
90 }
91 }
92}
93
94impl VdirCoroutine for VdirCollectionUpdate {
95 type Yield = VdirYield;
96 type Return = Result<(), VdirCollectionUpdateError>;
97
98 fn resume(&mut self, arg: Option<VdirReply>) -> VdirCoroutineState<Self::Yield, Self::Return> {
99 match (&mut self.state, arg) {
100 (State::Start(collection), None) => {
101 let collection = mem::take(collection);
102 let mut files = BTreeMap::new();
103 let mut renames = Vec::new();
104 let mut removals = BTreeSet::new();
105
106 let mut field = |name: &str, value: Option<String>| {
111 let final_path = collection.path.join(name);
112 match value.filter(|value| !value.is_empty()) {
113 Some(value) => {
114 let tmp_path = final_path.with_file_name(&format!("{name}.{TMP}"));
115 files.insert(tmp_path.clone(), value.into_bytes());
116 renames.push((tmp_path, final_path));
117 }
118 None => {
119 removals.insert(final_path);
120 }
121 }
122 };
123
124 field(DISPLAYNAME, collection.display_name.clone());
125 field(DESCRIPTION, collection.description.clone());
126 field(COLOR, collection.color.clone());
127
128 if files.is_empty() {
129 self.state = State::RemoveFiles;
130 return VdirCoroutineState::Yielded(VdirYield::WantsFileRemove(removals));
131 }
132
133 self.state = State::CreateFile { renames, removals };
134 VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files))
135 }
136 (State::CreateFile { renames, removals }, Some(VdirReply::FileCreate)) => {
137 let renames = mem::take(renames);
138 let removals = mem::take(removals);
139 self.state = State::Rename { removals };
140 VdirCoroutineState::Yielded(VdirYield::WantsRename(renames))
141 }
142 (State::Rename { removals }, Some(VdirReply::Rename)) => {
143 let removals = mem::take(removals);
144 if removals.is_empty() {
145 return VdirCoroutineState::Complete(Ok(()));
146 }
147
148 self.state = State::RemoveFiles;
149 VdirCoroutineState::Yielded(VdirYield::WantsFileRemove(removals))
150 }
151 (State::RemoveFiles, Some(VdirReply::FileRemove)) => {
152 VdirCoroutineState::Complete(Ok(()))
153 }
154 (_, arg) => {
155 let err = VdirCollectionUpdateError::UnexpectedArg(arg);
156 VdirCoroutineState::Complete(Err(err))
157 }
158 }
159 }
160}
161
162#[derive(Debug)]
163enum State {
164 Start(VdirCollection),
165 CreateFile {
166 renames: Vec<(VdirPath, VdirPath)>,
167 removals: BTreeSet<VdirPath>,
168 },
169 Rename {
170 removals: BTreeSet<VdirPath>,
171 },
172 RemoveFiles,
173}
174
175impl fmt::Display for State {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 match self {
178 Self::Start(_) => f.write_str("start"),
179 Self::CreateFile { .. } => f.write_str("write metadata into tmp"),
180 Self::Rename { .. } => f.write_str("rename into place"),
181 Self::RemoveFiles => f.write_str("remove cleared metadata"),
182 }
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn writes_temp_files_then_renames() {
192 let collection = VdirCollection {
193 path: VdirPath::from("root/contacts"),
194 display_name: Some("Contacts".into()),
195 description: None,
196 color: None,
197 };
198 let mut cor = VdirCollectionUpdate::new(collection, VdirCollectionUpdateOptions::default());
199
200 let files = match cor.resume(None) {
201 VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files)) => files,
202 state => panic!("expected WantsFileCreate, got {state:?}"),
203 };
204 let tmp = VdirPath::from("root/contacts/displayname.tmp");
205 assert!(files.contains_key(&tmp));
206
207 let pairs = match cor.resume(Some(VdirReply::FileCreate)) {
208 VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => pairs,
209 state => panic!("expected WantsRename, got {state:?}"),
210 };
211 assert_eq!(
212 pairs,
213 vec![(tmp, VdirPath::from("root/contacts/displayname"))]
214 );
215
216 let removals = match cor.resume(Some(VdirReply::Rename)) {
219 VdirCoroutineState::Yielded(VdirYield::WantsFileRemove(removals)) => removals,
220 state => panic!("expected WantsFileRemove, got {state:?}"),
221 };
222 assert!(removals.contains(&VdirPath::from("root/contacts/description")));
223 assert!(removals.contains(&VdirPath::from("root/contacts/color")));
224 assert!(!removals.contains(&VdirPath::from("root/contacts/displayname")));
225
226 match cor.resume(Some(VdirReply::FileRemove)) {
227 VdirCoroutineState::Complete(Ok(())) => {}
228 state => panic!("expected Complete(Ok), got {state:?}"),
229 }
230 }
231
232 #[test]
233 fn no_metadata_removes_every_file() {
234 let mut cor = VdirCollectionUpdate::new(
235 VdirCollection::from_path("root/contacts"),
236 VdirCollectionUpdateOptions::default(),
237 );
238
239 let removals = match cor.resume(None) {
242 VdirCoroutineState::Yielded(VdirYield::WantsFileRemove(removals)) => removals,
243 state => panic!("expected WantsFileRemove, got {state:?}"),
244 };
245 assert_eq!(removals.len(), 3);
246
247 match cor.resume(Some(VdirReply::FileRemove)) {
248 VdirCoroutineState::Complete(Ok(())) => {}
249 state => panic!("expected Complete(Ok), got {state:?}"),
250 }
251 }
252
253 #[test]
254 fn unexpected_reply_returns_error() {
255 let collection = VdirCollection {
256 path: VdirPath::from("root/contacts"),
257 display_name: Some("Contacts".into()),
258 description: None,
259 color: None,
260 };
261 let mut cor = VdirCollectionUpdate::new(collection, VdirCollectionUpdateOptions::default());
262 let _ = cor.resume(None);
263
264 let err = match cor.resume(Some(VdirReply::DirExists(BTreeMap::new()))) {
265 VdirCoroutineState::Complete(Err(err)) => err,
266 state => panic!("expected Complete(Err), got {state:?}"),
267 };
268 assert!(matches!(err, VdirCollectionUpdateError::UnexpectedArg(_)));
269 }
270}