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