Skip to main content

io_vdir/item/
get.rs

1//! I/O-free coroutine fetching a Vdir item by its ID.
2//!
3//! Locates the item file via [`VdirItemLocate`], then reads its contents.
4//!
5//! # Example
6//!
7//! ```rust,no_run
8//! use std::fs;
9//!
10//! use io_vdir::{coroutine::*, item::get::*};
11//!
12//! let opts = VdirItemGetOptions::default();
13//! let mut coroutine = VdirItemGet::new("/tmp/vdir/contacts", "alice", opts);
14//! let mut arg = None;
15//!
16//! let item = loop {
17//!     match coroutine.resume(arg.take()) {
18//!         VdirCoroutineState::Yielded(VdirYield::WantsFileExists(paths)) => {
19//!             let map = paths
20//!                 .into_iter()
21//!                 .map(|p| {
22//!                     let ok = fs::metadata(p.as_str()).map(|m| m.is_file()).unwrap_or(false);
23//!                     (p, ok)
24//!                 })
25//!                 .collect();
26//!             arg = Some(VdirReply::FileExists(map));
27//!         }
28//!         VdirCoroutineState::Yielded(VdirYield::WantsFileRead(paths)) => {
29//!             let map = paths
30//!                 .into_iter()
31//!                 .map(|p| {
32//!                     let bytes = fs::read(p.as_str()).unwrap_or_default();
33//!                     (p, bytes)
34//!                 })
35//!                 .collect();
36//!             arg = Some(VdirReply::FileRead(map));
37//!         }
38//!         VdirCoroutineState::Complete(Ok(item)) => break item,
39//!         VdirCoroutineState::Complete(Err(err)) => panic!("{err}"),
40//!         state => panic!("unexpected state {state:?}"),
41//!     }
42//! };
43//!
44//! println!("{} bytes", item.contents.len());
45//! ```
46
47use core::{fmt, mem};
48
49use alloc::{collections::BTreeSet, string::ToString};
50
51use thiserror::Error;
52
53use crate::{
54    coroutine::*,
55    item::{VdirItem, VdirItemKind, locate::*},
56    path::VdirPath,
57    vdir_try,
58};
59
60/// Failure causes during a [`VdirItemGet`] step.
61#[derive(Clone, Debug, Error)]
62pub enum VdirItemGetError {
63    /// The driver fed back a reply that does not match the pending
64    /// request.
65    #[error("Vdir item get failed: unexpected arg {0:?}")]
66    UnexpectedArg(Option<VdirReply>),
67
68    /// The inner locate coroutine failed.
69    #[error(transparent)]
70    Locate(#[from] VdirItemLocateError),
71}
72
73/// Options for [`VdirItemGet::new`].
74#[derive(Clone, Debug, Default, Eq, PartialEq)]
75pub struct VdirItemGetOptions {}
76
77/// Locates a Vdir item by ID and reads its contents.
78#[derive(Debug)]
79pub struct VdirItemGet {
80    state: State,
81    #[allow(dead_code)]
82    opts: VdirItemGetOptions,
83}
84
85impl VdirItemGet {
86    /// Creates a new coroutine that will retrieve item `id` from
87    /// `collection`.
88    pub fn new(
89        collection: impl Into<VdirPath>,
90        id: impl ToString,
91        opts: VdirItemGetOptions,
92    ) -> Self {
93        Self {
94            opts,
95            state: State::Locate(VdirItemLocate::new(
96                collection,
97                id,
98                VdirItemLocateOptions::default(),
99            )),
100        }
101    }
102}
103
104impl VdirCoroutine for VdirItemGet {
105    type Yield = VdirYield;
106    type Return = Result<VdirItem, VdirItemGetError>;
107
108    fn resume(&mut self, arg: Option<VdirReply>) -> VdirCoroutineState<Self::Yield, Self::Return> {
109        match (&mut self.state, arg) {
110            (State::Locate(c), arg) => {
111                let out = vdir_try!(c, arg);
112                let paths = BTreeSet::from_iter([out.path.clone()]);
113                self.state = State::AwaitRead {
114                    path: out.path,
115                    kind: out.kind,
116                };
117                VdirCoroutineState::Yielded(VdirYield::WantsFileRead(paths))
118            }
119            (State::AwaitRead { path, kind }, Some(VdirReply::FileRead(mut map))) => {
120                let path = mem::take(path);
121                let kind = *kind;
122                let contents = map.remove(&path).unwrap_or_default();
123                VdirCoroutineState::Complete(Ok(VdirItem {
124                    path,
125                    kind,
126                    contents,
127                }))
128            }
129            (_, arg) => {
130                let err = VdirItemGetError::UnexpectedArg(arg);
131                VdirCoroutineState::Complete(Err(err))
132            }
133        }
134    }
135}
136
137#[derive(Debug)]
138enum State {
139    Locate(VdirItemLocate),
140    AwaitRead { path: VdirPath, kind: VdirItemKind },
141}
142
143impl fmt::Display for State {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Self::Locate(_) => f.write_str("locate item"),
147            Self::AwaitRead { .. } => f.write_str("await read reply"),
148        }
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use alloc::collections::BTreeMap;
155
156    use super::*;
157
158    #[test]
159    fn locates_then_reads_contents() {
160        let mut cor = VdirItemGet::new("root/contacts", "alice", VdirItemGetOptions::default());
161
162        match cor.resume(None) {
163            VdirCoroutineState::Yielded(VdirYield::WantsFileExists(_)) => {}
164            state => panic!("expected WantsFileExists, got {state:?}"),
165        }
166
167        let vcf = VdirPath::from("root/contacts/alice.vcf");
168        let mut exists = BTreeMap::new();
169        exists.insert(vcf.clone(), true);
170        exists.insert(VdirPath::from("root/contacts/alice.ics"), false);
171
172        let paths = match cor.resume(Some(VdirReply::FileExists(exists))) {
173            VdirCoroutineState::Yielded(VdirYield::WantsFileRead(paths)) => paths,
174            state => panic!("expected WantsFileRead, got {state:?}"),
175        };
176        assert!(paths.contains(&vcf));
177
178        let mut contents = BTreeMap::new();
179        contents.insert(vcf.clone(), b"BEGIN:VCARD".to_vec());
180        let item = match cor.resume(Some(VdirReply::FileRead(contents))) {
181            VdirCoroutineState::Complete(Ok(item)) => item,
182            state => panic!("expected Complete(Ok), got {state:?}"),
183        };
184        assert_eq!(item.path, vcf);
185        assert_eq!(item.kind, VdirItemKind::Vcard);
186        assert_eq!(item.contents, b"BEGIN:VCARD");
187    }
188
189    #[test]
190    fn locate_error_is_forwarded() {
191        let mut cor = VdirItemGet::new("root/contacts", "alice", VdirItemGetOptions::default());
192        let _ = cor.resume(None);
193
194        let err = match cor.resume(Some(VdirReply::DirCreate)) {
195            VdirCoroutineState::Complete(Err(err)) => err,
196            state => panic!("expected Complete(Err), got {state:?}"),
197        };
198        assert!(matches!(err, VdirItemGetError::Locate(_)));
199    }
200}