io_vdir/collection/
update.rs1use core::{fmt, mem};
45
46use alloc::{collections::BTreeMap, vec::Vec};
47
48use thiserror::Error;
49
50use crate::{
51 collection::{COLOR, DESCRIPTION, DISPLAYNAME, VdirCollection},
52 coroutine::*,
53 item::TMP,
54 path::VdirPath,
55};
56
57#[derive(Clone, Debug, Error)]
59pub enum VdirCollectionUpdateError {
60 #[error("Vdir collection update failed: unexpected arg {0:?}")]
63 UnexpectedArg(Option<VdirReply>),
64}
65
66#[derive(Clone, Debug, Default, Eq, PartialEq)]
68pub struct VdirCollectionUpdateOptions {}
69
70#[derive(Debug)]
72pub struct VdirCollectionUpdate {
73 state: State,
74 #[allow(dead_code)]
75 opts: VdirCollectionUpdateOptions,
76}
77
78impl VdirCollectionUpdate {
79 pub fn new(collection: VdirCollection, opts: VdirCollectionUpdateOptions) -> Self {
82 Self {
83 opts,
84 state: State::Start(collection),
85 }
86 }
87}
88
89impl VdirCoroutine for VdirCollectionUpdate {
90 type Yield = VdirYield;
91 type Return = Result<(), VdirCollectionUpdateError>;
92
93 fn resume(&mut self, arg: Option<VdirReply>) -> VdirCoroutineState<Self::Yield, Self::Return> {
94 match (&mut self.state, arg) {
95 (State::Start(collection), None) => {
96 let collection = mem::take(collection);
97 let mut files = BTreeMap::new();
98 let mut renames = Vec::new();
99
100 if let Some(name) = collection.display_name.filter(|s| !s.is_empty()) {
101 let final_path = collection.path.join(DISPLAYNAME);
102 let tmp_path = final_path.with_file_name(&format!("{DISPLAYNAME}.{TMP}"));
103 files.insert(tmp_path.clone(), name.into_bytes());
104 renames.push((tmp_path, final_path));
105 }
106
107 if let Some(desc) = collection.description.filter(|s| !s.is_empty()) {
108 let final_path = collection.path.join(DESCRIPTION);
109 let tmp_path = final_path.with_file_name(&format!("{DESCRIPTION}.{TMP}"));
110 files.insert(tmp_path.clone(), desc.into_bytes());
111 renames.push((tmp_path, final_path));
112 }
113
114 if let Some(color) = collection.color.filter(|s| !s.is_empty()) {
115 let final_path = collection.path.join(COLOR);
116 let tmp_path = final_path.with_file_name(&format!("{COLOR}.{TMP}"));
117 files.insert(tmp_path.clone(), color.into_bytes());
118 renames.push((tmp_path, final_path));
119 }
120
121 if files.is_empty() {
122 return VdirCoroutineState::Complete(Ok(()));
123 }
124
125 self.state = State::AwaitFileCreate { renames };
126 VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files))
127 }
128 (State::AwaitFileCreate { renames }, Some(VdirReply::FileCreate)) => {
129 let renames = mem::take(renames);
130 self.state = State::AwaitRename;
131 VdirCoroutineState::Yielded(VdirYield::WantsRename(renames))
132 }
133 (State::AwaitRename, Some(VdirReply::Rename)) => VdirCoroutineState::Complete(Ok(())),
134 (_, arg) => {
135 let err = VdirCollectionUpdateError::UnexpectedArg(arg);
136 VdirCoroutineState::Complete(Err(err))
137 }
138 }
139 }
140}
141
142#[derive(Debug)]
143enum State {
144 Start(VdirCollection),
145 AwaitFileCreate { renames: Vec<(VdirPath, VdirPath)> },
146 AwaitRename,
147}
148
149impl fmt::Display for State {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 match self {
152 Self::Start(_) => f.write_str("start"),
153 Self::AwaitFileCreate { .. } => f.write_str("await file create reply"),
154 Self::AwaitRename => f.write_str("await rename reply"),
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn writes_temp_files_then_renames() {
165 let collection = VdirCollection {
166 path: VdirPath::from("root/contacts"),
167 display_name: Some("Contacts".into()),
168 description: None,
169 color: None,
170 };
171 let mut cor = VdirCollectionUpdate::new(collection, VdirCollectionUpdateOptions::default());
172
173 let files = match cor.resume(None) {
174 VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files)) => files,
175 state => panic!("expected WantsFileCreate, got {state:?}"),
176 };
177 let tmp = VdirPath::from("root/contacts/displayname.tmp");
178 assert!(files.contains_key(&tmp));
179
180 let pairs = match cor.resume(Some(VdirReply::FileCreate)) {
181 VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => pairs,
182 state => panic!("expected WantsRename, got {state:?}"),
183 };
184 assert_eq!(
185 pairs,
186 vec![(tmp, VdirPath::from("root/contacts/displayname"))]
187 );
188
189 match cor.resume(Some(VdirReply::Rename)) {
190 VdirCoroutineState::Complete(Ok(())) => {}
191 state => panic!("expected Complete(Ok), got {state:?}"),
192 }
193 }
194
195 #[test]
196 fn no_metadata_completes_immediately() {
197 let mut cor = VdirCollectionUpdate::new(
198 VdirCollection::from_path("root/contacts"),
199 VdirCollectionUpdateOptions::default(),
200 );
201
202 match cor.resume(None) {
203 VdirCoroutineState::Complete(Ok(())) => {}
204 state => panic!("expected Complete(Ok), got {state:?}"),
205 }
206 }
207
208 #[test]
209 fn unexpected_reply_returns_error() {
210 let collection = VdirCollection {
211 path: VdirPath::from("root/contacts"),
212 display_name: Some("Contacts".into()),
213 description: None,
214 color: None,
215 };
216 let mut cor = VdirCollectionUpdate::new(collection, VdirCollectionUpdateOptions::default());
217 let _ = cor.resume(None);
218
219 let err = match cor.resume(Some(VdirReply::DirExists(BTreeMap::new()))) {
220 VdirCoroutineState::Complete(Err(err)) => err,
221 state => panic!("expected Complete(Err), got {state:?}"),
222 };
223 assert!(matches!(err, VdirCollectionUpdateError::UnexpectedArg(_)));
224 }
225}