Skip to main content

io_vdir/item/
move.rs

1//! I/O-free coroutine moving a Vdir item across collections.
2//!
3//! Locates the source item via [`VdirItemLocate`], then renames it into the
4//! target collection keeping the same id and extension.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use std::fs;
10//!
11//! use io_vdir::{coroutine::*, item::r#move::*};
12//!
13//! let opts = VdirItemMoveOptions::default();
14//! let mut coroutine = VdirItemMove::new("/tmp/vdir/contacts", "/tmp/vdir/work", "alice", opts);
15//! let mut arg = None;
16//!
17//! loop {
18//!     match coroutine.resume(arg.take()) {
19//!         VdirCoroutineState::Yielded(VdirYield::WantsFileExists(paths)) => {
20//!             let map = paths
21//!                 .into_iter()
22//!                 .map(|p| {
23//!                     let ok = fs::metadata(p.as_str()).map(|m| m.is_file()).unwrap_or(false);
24//!                     (p, ok)
25//!                 })
26//!                 .collect();
27//!             arg = Some(VdirReply::FileExists(map));
28//!         }
29//!         VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => {
30//!             for (from, to) in pairs {
31//!                 fs::rename(from.as_str(), to.as_str()).unwrap();
32//!             }
33//!             arg = Some(VdirReply::Rename);
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;
43
44use alloc::string::{String, ToString};
45
46use thiserror::Error;
47
48use crate::{coroutine::*, item::locate::*, path::VdirPath, vdir_try};
49
50/// Failure causes during a [`VdirItemMove`] step.
51#[derive(Clone, Debug, Error)]
52pub enum VdirItemMoveError {
53    /// The driver fed back a reply that does not match the pending
54    /// request.
55    #[error("Vdir item move failed: unexpected arg {0:?}")]
56    UnexpectedArg(Option<VdirReply>),
57
58    /// The inner locate coroutine failed.
59    #[error(transparent)]
60    Locate(#[from] VdirItemLocateError),
61}
62
63/// Options for [`VdirItemMove::new`].
64#[derive(Clone, Debug, Default, Eq, PartialEq)]
65pub struct VdirItemMoveOptions {}
66
67/// Moves a Vdir item from a source into a target collection.
68#[derive(Debug)]
69pub struct VdirItemMove {
70    state: State,
71    #[allow(dead_code)]
72    opts: VdirItemMoveOptions,
73}
74
75impl VdirItemMove {
76    /// Creates a new coroutine that will move item `id` from `source`
77    /// into `target`. The item keeps the same id and extension in the
78    /// target collection.
79    pub fn new(
80        source: impl Into<VdirPath>,
81        target: impl Into<VdirPath>,
82        id: impl ToString,
83        opts: VdirItemMoveOptions,
84    ) -> Self {
85        let id = id.to_string();
86        let inner = VdirItemLocate::new(source, &id, VdirItemLocateOptions::default());
87        Self {
88            opts,
89            state: State::Locate {
90                target: target.into(),
91                id,
92                inner,
93            },
94        }
95    }
96}
97
98impl VdirCoroutine for VdirItemMove {
99    type Yield = VdirYield;
100    type Return = Result<(), VdirItemMoveError>;
101
102    fn resume(&mut self, arg: Option<VdirReply>) -> VdirCoroutineState<Self::Yield, Self::Return> {
103        match (&mut self.state, arg) {
104            (State::Locate { target, id, inner }, arg) => {
105                let out = vdir_try!(inner, arg);
106                let ext = out.kind.extension();
107                let target_path = target.join(&format!("{id}.{ext}"));
108                let pairs = vec![(out.path, target_path)];
109                self.state = State::AwaitRename;
110                VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs))
111            }
112            (State::AwaitRename, Some(VdirReply::Rename)) => VdirCoroutineState::Complete(Ok(())),
113            (_, arg) => {
114                let err = VdirItemMoveError::UnexpectedArg(arg);
115                VdirCoroutineState::Complete(Err(err))
116            }
117        }
118    }
119}
120
121#[derive(Debug)]
122enum State {
123    Locate {
124        target: VdirPath,
125        id: String,
126        inner: VdirItemLocate,
127    },
128    AwaitRename,
129}
130
131impl fmt::Display for State {
132    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133        match self {
134            Self::Locate { .. } => f.write_str("locate source item"),
135            Self::AwaitRename => f.write_str("await rename reply"),
136        }
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use alloc::collections::BTreeMap;
143
144    use super::*;
145
146    #[test]
147    fn moves_located_source_to_target() {
148        let mut cor =
149            VdirItemMove::new("root/a", "root/b", "alice", VdirItemMoveOptions::default());
150
151        match cor.resume(None) {
152            VdirCoroutineState::Yielded(VdirYield::WantsFileExists(_)) => {}
153            state => panic!("expected WantsFileExists, got {state:?}"),
154        }
155
156        let ics = VdirPath::from("root/a/alice.ics");
157        let mut exists = BTreeMap::new();
158        exists.insert(VdirPath::from("root/a/alice.vcf"), false);
159        exists.insert(ics.clone(), true);
160
161        let pairs = match cor.resume(Some(VdirReply::FileExists(exists))) {
162            VdirCoroutineState::Yielded(VdirYield::WantsRename(pairs)) => pairs,
163            state => panic!("expected WantsRename, got {state:?}"),
164        };
165        assert_eq!(pairs, vec![(ics, VdirPath::from("root/b/alice.ics"))]);
166
167        match cor.resume(Some(VdirReply::Rename)) {
168            VdirCoroutineState::Complete(Ok(())) => {}
169            state => panic!("expected Complete(Ok), got {state:?}"),
170        }
171    }
172
173    #[test]
174    fn locate_error_is_forwarded() {
175        let mut cor =
176            VdirItemMove::new("root/a", "root/b", "alice", VdirItemMoveOptions::default());
177        let _ = cor.resume(None);
178
179        let err = match cor.resume(Some(VdirReply::DirCreate)) {
180            VdirCoroutineState::Complete(Err(err)) => err,
181            state => panic!("expected Complete(Err), got {state:?}"),
182        };
183        assert!(matches!(err, VdirItemMoveError::Locate(_)));
184    }
185}