Skip to main content

io_vdir/collection/
rename.rs

1//! I/O-free coroutine renaming a Vdir collection directory.
2//!
3//! # Example
4//!
5//! ```rust,no_run
6//! use std::fs;
7//!
8//! use io_vdir::{collection::rename::*, coroutine::*};
9//!
10//! let opts = VdirCollectionRenameOptions::default();
11//! let mut coroutine = VdirCollectionRename::new("/tmp/vdir/contacts", "people", opts);
12//! let mut arg = None;
13//!
14//! loop {
15//!     match coroutine.resume(arg.take()) {
16//!         VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => {
17//!             for (from, to) in pairs {
18//!                 fs::rename(from.as_str(), to.as_str()).unwrap();
19//!             }
20//!             arg = Some(VdirReply::Rename);
21//!         }
22//!         VdirCoroutineState::Complete(Ok(())) => break,
23//!         VdirCoroutineState::Complete(Err(err)) => panic!("{err}"),
24//!         state => panic!("unexpected state {state:?}"),
25//!     }
26//! }
27//! ```
28
29use core::{fmt, mem};
30
31use alloc::{string::ToString, vec::Vec};
32
33use thiserror::Error;
34
35use crate::{coroutine::*, path::VdirPath};
36
37/// Failure causes during a [`VdirCollectionRename`] step.
38#[derive(Clone, Debug, Error)]
39pub enum VdirCollectionRenameError {
40    /// The driver fed back a reply that does not match the pending
41    /// request.
42    #[error("Vdir collection rename failed: unexpected arg {0:?}")]
43    UnexpectedArg(Option<VdirReply>),
44}
45
46/// Options for [`VdirCollectionRename::new`].
47#[derive(Clone, Debug, Default, Eq, PartialEq)]
48pub struct VdirCollectionRenameOptions {}
49
50/// Renames the collection at `path` to `name`, keeping the same parent
51/// directory.
52#[derive(Debug)]
53pub struct VdirCollectionRename {
54    state: State,
55    #[allow(dead_code)]
56    opts: VdirCollectionRenameOptions,
57}
58
59impl VdirCollectionRename {
60    /// Creates a new coroutine that will rename the collection at
61    /// `path` to `name` (keeping the same parent directory).
62    pub fn new(
63        path: impl Into<VdirPath>,
64        name: impl ToString,
65        opts: VdirCollectionRenameOptions,
66    ) -> Self {
67        let from = path.into();
68        let to = from.with_file_name(&name.to_string());
69
70        Self {
71            opts,
72            state: State::Start {
73                pairs: vec![(from, to)],
74            },
75        }
76    }
77}
78
79impl VdirCoroutine for VdirCollectionRename {
80    type Yield = VdirYield;
81    type Return = Result<(), VdirCollectionRenameError>;
82
83    fn resume(&mut self, arg: Option<VdirReply>) -> VdirCoroutineState<Self::Yield, Self::Return> {
84        match (&mut self.state, arg) {
85            (State::Start { pairs }, None) => {
86                let pairs = mem::take(pairs);
87                self.state = State::AwaitRename;
88                VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs))
89            }
90            (State::AwaitRename, Some(VdirReply::Rename)) => VdirCoroutineState::Complete(Ok(())),
91            (_, arg) => {
92                let err = VdirCollectionRenameError::UnexpectedArg(arg);
93                VdirCoroutineState::Complete(Err(err))
94            }
95        }
96    }
97}
98
99#[derive(Debug)]
100enum State {
101    Start { pairs: Vec<(VdirPath, VdirPath)> },
102    AwaitRename,
103}
104
105impl fmt::Display for State {
106    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107        match self {
108            Self::Start { .. } => f.write_str("start"),
109            Self::AwaitRename => f.write_str("await rename reply"),
110        }
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn renames_within_same_parent() {
120        let mut cor = VdirCollectionRename::new(
121            "root/contacts",
122            "people",
123            VdirCollectionRenameOptions::default(),
124        );
125
126        let pairs = match cor.resume(None) {
127            VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => pairs,
128            state => panic!("expected WantsRename, got {state:?}"),
129        };
130        assert_eq!(
131            pairs,
132            vec![(
133                VdirPath::from("root/contacts"),
134                VdirPath::from("root/people")
135            )]
136        );
137
138        match cor.resume(Some(VdirReply::Rename)) {
139            VdirCoroutineState::Complete(Ok(())) => {}
140            state => panic!("expected Complete(Ok), got {state:?}"),
141        }
142    }
143
144    #[test]
145    fn unexpected_reply_returns_error() {
146        let mut cor = VdirCollectionRename::new(
147            "root/contacts",
148            "people",
149            VdirCollectionRenameOptions::default(),
150        );
151        let _ = cor.resume(None);
152
153        let err = match cor.resume(Some(VdirReply::DirCreate)) {
154            VdirCoroutineState::Complete(Err(err)) => err,
155            state => panic!("expected Complete(Err), got {state:?}"),
156        };
157        assert!(matches!(err, VdirCollectionRenameError::UnexpectedArg(_)));
158    }
159}