Skip to main content

io_vdir/collection/
create.rs

1//! I/O-free coroutine creating a Vdir collection.
2//!
3//! Creates the collection directory first; then, when the collection
4//! carries metadata (`display_name`, `description` or `color`), writes
5//! the corresponding marker files.
6//!
7//! Each marker file lands on its `.tmp` sibling and is renamed into
8//! place, as [`VdirCollectionUpdate`] writes them, so a reader
9//! scanning the new collection reads a whole value or no file at all.
10//!
11//! [`VdirCollectionUpdate`]: crate::collection::update::VdirCollectionUpdate
12//!
13//! # Example
14//!
15//! ```rust,no_run
16//! use std::fs;
17//!
18//! use io_vdir::{
19//!     collection::{create::*, VdirCollection},
20//!     coroutine::*,
21//! };
22//!
23//! let opts = VdirCollectionCreateOptions::default();
24//! let mut coroutine = VdirCollectionCreate::new(VdirCollection::from_path("/tmp/vdir/contacts"), opts);
25//! let mut arg = None;
26//!
27//! loop {
28//!     match coroutine.resume(arg.take()) {
29//!         VdirCoroutineState::Yielded(VdirYield::WantsDirCreate(paths)) => {
30//!             for path in paths {
31//!                 fs::create_dir_all(path.as_str()).unwrap();
32//!             }
33//!             arg = Some(VdirReply::DirCreate);
34//!         }
35//!         VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files)) => {
36//!             for (path, bytes) in files {
37//!                 fs::write(path.as_str(), bytes).unwrap();
38//!             }
39//!             arg = Some(VdirReply::FileCreate);
40//!         }
41//!         VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => {
42//!             for (from, to) in pairs {
43//!                 fs::rename(from.as_str(), to.as_str()).unwrap();
44//!             }
45//!             arg = Some(VdirReply::Rename);
46//!         }
47//!         VdirCoroutineState::Complete(Ok(())) => break,
48//!         VdirCoroutineState::Complete(Err(err)) => panic!("{err}"),
49//!         state => panic!("unexpected state {state:?}"),
50//!     }
51//! }
52//! ```
53
54use 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/// Failure causes during a [`VdirCollectionCreate`] step.
72#[derive(Clone, Debug, Error)]
73pub enum VdirCollectionCreateError {
74    /// The driver fed back a reply that does not match the pending
75    /// request.
76    #[error("Vdir collection create failed: unexpected arg {0:?}")]
77    UnexpectedArg(Option<VdirReply>),
78}
79
80/// Options for [`VdirCollectionCreate::new`].
81#[derive(Clone, Debug, Default, Eq, PartialEq)]
82pub struct VdirCollectionCreateOptions {}
83
84/// Creates a Vdir collection directory and its metadata marker files.
85#[derive(Debug)]
86pub struct VdirCollectionCreate {
87    state: State,
88    #[allow(dead_code)]
89    opts: VdirCollectionCreateOptions,
90}
91
92impl VdirCollectionCreate {
93    /// Creates a new coroutine that will create `collection`.
94    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        // The values land on their tmp siblings, never on the marker
206        // names a reader looks for.
207        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}