Skip to main content

io_vdir/item/
locate.rs

1//! I/O-free coroutine locating a Vdir item file by its ID.
2//!
3//! Probes `<collection>/<id>.vcf` and `<collection>/<id>.ics` in a
4//! single batched file-exists request.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use std::fs;
10//!
11//! use io_vdir::{coroutine::*, item::locate::*};
12//!
13//! let opts = VdirItemLocateOptions::default();
14//! let mut coroutine = VdirItemLocate::new("/tmp/vdir/contacts", "alice", opts);
15//! let mut arg = None;
16//!
17//! let out = 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::Complete(Ok(out)) => break out,
30//!         VdirCoroutineState::Complete(Err(err)) => panic!("{err}"),
31//!         state => panic!("unexpected state {state:?}"),
32//!     }
33//! };
34//!
35//! println!("located {} ({:?})", out.path, out.kind);
36//! ```
37
38use core::{fmt, mem};
39
40use alloc::{
41    collections::BTreeSet,
42    string::{String, ToString},
43};
44
45use thiserror::Error;
46
47use crate::{
48    coroutine::*,
49    item::{ICS, VCF, VdirItemKind},
50    path::VdirPath,
51};
52
53/// Failure causes during a [`VdirItemLocate`] step.
54#[derive(Clone, Debug, Error)]
55pub enum VdirItemLocateError {
56    /// The driver fed back a reply that does not match the pending
57    /// request.
58    #[error("Vdir item locate failed: unexpected arg {0:?}")]
59    UnexpectedArg(Option<VdirReply>),
60
61    /// No item with the given id exists in the collection.
62    #[error("Vdir item locate failed: item {0} not found")]
63    NotFound(String),
64}
65
66/// Successful output of [`VdirItemLocate`].
67#[derive(Clone, Debug)]
68pub struct VdirItemLocateOutput {
69    /// On-disk path of the located item file.
70    pub path: VdirPath,
71    /// Kind of the located item, derived from its extension.
72    pub kind: VdirItemKind,
73}
74
75/// Options for [`VdirItemLocate::new`].
76#[derive(Clone, Debug, Default, Eq, PartialEq)]
77pub struct VdirItemLocateOptions {}
78
79/// Locates a Vdir item file by its ID.
80#[derive(Clone, Debug)]
81pub struct VdirItemLocate {
82    state: State,
83    #[allow(dead_code)]
84    opts: VdirItemLocateOptions,
85}
86
87impl VdirItemLocate {
88    /// Creates a new coroutine that will search for item `id` inside
89    /// `collection`.
90    pub fn new(
91        collection: impl Into<VdirPath>,
92        id: impl ToString,
93        opts: VdirItemLocateOptions,
94    ) -> Self {
95        Self {
96            opts,
97            state: State::Start {
98                collection: collection.into(),
99                id: id.to_string(),
100            },
101        }
102    }
103}
104
105impl VdirCoroutine for VdirItemLocate {
106    type Yield = VdirYield;
107    type Return = Result<VdirItemLocateOutput, VdirItemLocateError>;
108
109    fn resume(&mut self, arg: Option<VdirReply>) -> VdirCoroutineState<Self::Yield, Self::Return> {
110        match (&mut self.state, arg) {
111            (State::Start { collection, id }, None) => {
112                let id = mem::take(id);
113                let vcf_path = collection.join(&format!("{id}.{VCF}"));
114                let ics_path = collection.join(&format!("{id}.{ICS}"));
115
116                let probes = BTreeSet::from_iter([vcf_path.clone(), ics_path.clone()]);
117                self.state = State::AwaitProbe {
118                    id,
119                    vcf_path,
120                    ics_path,
121                };
122                VdirCoroutineState::Yielded(VdirYield::WantsFileExists(probes))
123            }
124            (
125                State::AwaitProbe {
126                    id,
127                    vcf_path,
128                    ics_path,
129                },
130                Some(VdirReply::FileExists(probes)),
131            ) => {
132                if probes.get(vcf_path).copied().unwrap_or(false) {
133                    let out = VdirItemLocateOutput {
134                        path: mem::take(vcf_path),
135                        kind: VdirItemKind::Vcard,
136                    };
137                    return VdirCoroutineState::Complete(Ok(out));
138                }
139
140                if probes.get(ics_path).copied().unwrap_or(false) {
141                    let out = VdirItemLocateOutput {
142                        path: mem::take(ics_path),
143                        kind: VdirItemKind::Ical,
144                    };
145                    return VdirCoroutineState::Complete(Ok(out));
146                }
147
148                let err = VdirItemLocateError::NotFound(mem::take(id));
149                VdirCoroutineState::Complete(Err(err))
150            }
151            (_, arg) => {
152                let err = VdirItemLocateError::UnexpectedArg(arg);
153                VdirCoroutineState::Complete(Err(err))
154            }
155        }
156    }
157}
158
159#[derive(Clone, Debug)]
160enum State {
161    Start {
162        collection: VdirPath,
163        id: String,
164    },
165    AwaitProbe {
166        id: String,
167        vcf_path: VdirPath,
168        ics_path: VdirPath,
169    },
170}
171
172impl fmt::Display for State {
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        match self {
175            Self::Start { .. } => f.write_str("start"),
176            Self::AwaitProbe { .. } => f.write_str("await probe reply"),
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use alloc::collections::BTreeMap;
184
185    use super::*;
186
187    #[test]
188    fn found_as_vcard_returns_ok() {
189        let mut cor =
190            VdirItemLocate::new("root/contacts", "alice", VdirItemLocateOptions::default());
191
192        let probes = expect_wants_file_exists(&mut cor);
193        let vcf = VdirPath::from("root/contacts/alice.vcf");
194        let ics = VdirPath::from("root/contacts/alice.ics");
195        assert!(probes.contains(&vcf));
196        assert!(probes.contains(&ics));
197
198        let mut map = BTreeMap::new();
199        map.insert(vcf.clone(), true);
200        map.insert(ics, false);
201        let out = expect_complete_ok(&mut cor, Some(VdirReply::FileExists(map)));
202        assert_eq!(out.path, vcf);
203        assert_eq!(out.kind, VdirItemKind::Vcard);
204    }
205
206    #[test]
207    fn not_found_returns_error() {
208        let mut cor =
209            VdirItemLocate::new("root/contacts", "alice", VdirItemLocateOptions::default());
210        let _ = expect_wants_file_exists(&mut cor);
211
212        let mut map = BTreeMap::new();
213        map.insert(VdirPath::from("root/contacts/alice.vcf"), false);
214        map.insert(VdirPath::from("root/contacts/alice.ics"), false);
215        let err = expect_complete_err(&mut cor, Some(VdirReply::FileExists(map)));
216        assert!(matches!(err, VdirItemLocateError::NotFound(_)));
217    }
218
219    #[test]
220    fn unexpected_reply_returns_error() {
221        let mut cor =
222            VdirItemLocate::new("root/contacts", "alice", VdirItemLocateOptions::default());
223        let _ = expect_wants_file_exists(&mut cor);
224
225        let err = expect_complete_err(&mut cor, Some(VdirReply::DirCreate));
226        assert!(matches!(err, VdirItemLocateError::UnexpectedArg(_)));
227    }
228
229    fn expect_wants_file_exists(cor: &mut VdirItemLocate) -> BTreeSet<VdirPath> {
230        match cor.resume(None) {
231            VdirCoroutineState::Yielded(VdirYield::WantsFileExists(paths)) => paths,
232            state => panic!("expected WantsFileExists, got {state:?}"),
233        }
234    }
235
236    fn expect_complete_ok(
237        cor: &mut VdirItemLocate,
238        arg: Option<VdirReply>,
239    ) -> VdirItemLocateOutput {
240        match cor.resume(arg) {
241            VdirCoroutineState::Complete(Ok(out)) => out,
242            state => panic!("expected Complete(Ok), got {state:?}"),
243        }
244    }
245
246    fn expect_complete_err(
247        cor: &mut VdirItemLocate,
248        arg: Option<VdirReply>,
249    ) -> VdirItemLocateError {
250        match cor.resume(arg) {
251            VdirCoroutineState::Complete(Err(err)) => err,
252            state => panic!("expected Complete(Err), got {state:?}"),
253        }
254    }
255}