1use 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#[derive(Clone, Debug, Error)]
55pub enum VdirItemLocateError {
56 #[error("Vdir item locate failed: unexpected arg {0:?}")]
59 UnexpectedArg(Option<VdirReply>),
60
61 #[error("Vdir item locate failed: item {0} not found")]
63 NotFound(String),
64}
65
66#[derive(Clone, Debug)]
68pub struct VdirItemLocateOutput {
69 pub path: VdirPath,
71 pub kind: VdirItemKind,
73}
74
75#[derive(Clone, Debug, Default, Eq, PartialEq)]
77pub struct VdirItemLocateOptions {}
78
79#[derive(Clone, Debug)]
81pub struct VdirItemLocate {
82 state: State,
83 #[allow(dead_code)]
84 opts: VdirItemLocateOptions,
85}
86
87impl VdirItemLocate {
88 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}