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