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//! # Example
8//!
9//! ```rust,no_run
10//! use std::fs;
11//!
12//! use io_vdir::{
13//!     collection::{create::*, VdirCollection},
14//!     coroutine::*,
15//! };
16//!
17//! let opts = VdirCollectionCreateOptions::default();
18//! let mut coroutine = VdirCollectionCreate::new(VdirCollection::from_path("/tmp/vdir/contacts"), opts);
19//! let mut arg = None;
20//!
21//! loop {
22//!     match coroutine.resume(arg.take()) {
23//!         VdirCoroutineState::Yielded(VdirYield::WantsDirCreate(paths)) => {
24//!             for path in paths {
25//!                 fs::create_dir_all(path.as_str()).unwrap();
26//!             }
27//!             arg = Some(VdirReply::DirCreate);
28//!         }
29//!         VdirCoroutineState::Yielded(VdirYield::WantsFileCreate(files)) => {
30//!             for (path, bytes) in files {
31//!                 fs::write(path.as_str(), bytes).unwrap();
32//!             }
33//!             arg = Some(VdirReply::FileCreate);
34//!         }
35//!         VdirCoroutineState::Complete(Ok(())) => break,
36//!         VdirCoroutineState::Complete(Err(err)) => panic!("{err}"),
37//!         state => panic!("unexpected state {state:?}"),
38//!     }
39//! }
40//! ```
41
42use 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/// Failure causes during a [`VdirCollectionCreate`] step.
54#[derive(Clone, Debug, Error)]
55pub enum VdirCollectionCreateError {
56    /// The driver fed back a reply that does not match the pending
57    /// request.
58    #[error("Vdir collection create failed: unexpected arg {0:?}")]
59    UnexpectedArg(Option<VdirReply>),
60}
61
62/// Options for [`VdirCollectionCreate::new`].
63#[derive(Clone, Debug, Default, Eq, PartialEq)]
64pub struct VdirCollectionCreateOptions {}
65
66/// Creates a Vdir collection directory and its metadata marker files.
67#[derive(Debug)]
68pub struct VdirCollectionCreate {
69    state: State,
70    #[allow(dead_code)]
71    opts: VdirCollectionCreateOptions,
72}
73
74impl VdirCollectionCreate {
75    /// Creates a new coroutine that will create `collection`.
76    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}