1use core::{fmt, marker::PhantomData};
58
59use alloc::{string::String, vec::Vec};
60
61use log::trace;
62use secrecy::SecretString;
63use serde::{Deserialize, Serialize, de::DeserializeOwned};
64use thiserror::Error;
65use url::Url;
66
67use crate::{
68 coroutine::*,
69 jmap_try,
70 rfc8620::{JmapBatch, JmapMethodError, send::*},
71};
72
73#[derive(Debug, Error)]
75pub enum JmapGetError {
76 #[error("JMAP Foo/get failed: missing response in method_responses")]
77 MissingResponse,
78 #[error("JMAP Foo/get failed: {0}")]
79 Send(#[from] JmapSendError),
80 #[error("JMAP Foo/get failed: serialize args: {0}")]
81 SerializeArgs(#[source] serde_json::Error),
82 #[error("JMAP Foo/get failed: parse response: {0}")]
83 ParseResponse(#[source] serde_json::Error),
84 #[error("JMAP Foo/get failed: {0}")]
85 Method(#[from] JmapMethodError),
86}
87
88#[derive(Clone, Debug, Default)]
90pub struct JmapGetOptions {
91 pub ids: Option<Vec<String>>,
93 pub properties: Option<Vec<String>>,
95}
96
97#[derive(Clone, Debug)]
99pub struct JmapGetOutput<T> {
100 pub list: Vec<T>,
101 pub not_found: Vec<String>,
102 pub state: String,
103 pub keep_alive: bool,
104}
105
106pub struct JmapGet<T> {
108 state: State,
109 _phantom: PhantomData<T>,
110}
111
112impl<T: DeserializeOwned> JmapGet<T> {
113 pub fn new(
115 account_id: String,
116 http_auth: &SecretString,
117 api_url: &Url,
118 method: impl Into<String>,
119 capabilities: Vec<String>,
120 opts: JmapGetOptions,
121 ) -> Result<Self, JmapGetError> {
122 let args = serde_json::to_value(GetArgs {
123 account_id: &account_id,
124 ids: opts.ids.as_deref(),
125 properties: opts.properties.as_deref(),
126 })
127 .map_err(JmapGetError::SerializeArgs)?;
128
129 let mut batch = JmapBatch::new();
130 batch.add(method, args);
131
132 let request = batch.into_request(capabilities);
133
134 Ok(Self {
135 state: State::Send(JmapSend::new(http_auth, api_url, request)?),
136 _phantom: PhantomData,
137 })
138 }
139
140 pub fn from_send(send: JmapSend) -> Self {
143 Self {
144 state: State::Send(send),
145 _phantom: PhantomData,
146 }
147 }
148}
149
150impl<T: DeserializeOwned> JmapCoroutine for JmapGet<T> {
151 type Yield = JmapYield;
152 type Return = Result<JmapGetOutput<T>, JmapGetError>;
153
154 fn resume(&mut self, arg: Option<&[u8]>) -> JmapCoroutineState<Self::Yield, Self::Return> {
155 trace!("get: {}", self.state);
156
157 match &mut self.state {
158 State::Send(send) => {
159 let JmapSendOutput {
160 response,
161 keep_alive,
162 } = jmap_try!(send, arg);
163
164 let Some((name, args, _)) = response.method_responses.into_iter().next() else {
165 return JmapCoroutineState::Complete(Err(JmapGetError::MissingResponse));
166 };
167
168 if name == "error" {
169 let err = serde_json::from_value::<JmapMethodError>(args)
170 .unwrap_or(JmapMethodError::Unknown);
171 return JmapCoroutineState::Complete(Err(err.into()));
172 }
173
174 match serde_json::from_value::<GetResponse<T>>(args) {
175 Ok(r) => JmapCoroutineState::Complete(Ok(JmapGetOutput {
176 list: r.list,
177 not_found: r.not_found,
178 state: r.state,
179 keep_alive,
180 })),
181 Err(err) => JmapCoroutineState::Complete(Err(JmapGetError::ParseResponse(err))),
182 }
183 }
184 }
185 }
186}
187
188enum State {
189 Send(JmapSend),
190}
191
192impl fmt::Display for State {
193 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194 match self {
195 Self::Send(_) => f.write_str("send"),
196 }
197 }
198}
199
200#[derive(Serialize)]
201#[serde(rename_all = "camelCase")]
202struct GetArgs<'a> {
203 account_id: &'a str,
204 #[serde(skip_serializing_if = "Option::is_none")]
205 ids: Option<&'a [String]>,
206 #[serde(skip_serializing_if = "Option::is_none")]
207 properties: Option<&'a [String]>,
208}
209
210#[derive(Deserialize)]
211#[serde(rename_all = "camelCase")]
212struct GetResponse<T> {
213 list: Vec<T>,
214 #[serde(default)]
215 not_found: Vec<String>,
216 state: String,
217}
218
219#[cfg(test)]
220mod tests {
221 use alloc::{format, string::ToString, vec};
222
223 use super::*;
224
225 #[derive(Debug, Deserialize, PartialEq)]
226 struct Probe {
227 id: String,
228 }
229
230 fn make_auth() -> SecretString {
231 SecretString::from("Bearer test")
232 }
233
234 fn make_url() -> Url {
235 "https://api.example.com/jmap/".parse().unwrap()
236 }
237
238 fn make_get() -> JmapGet<Probe> {
239 JmapGet::<Probe>::new(
240 "a1".to_string(),
241 &make_auth(),
242 &make_url(),
243 "Mailbox/get",
244 vec!["urn:ietf:params:jmap:mail".to_string()],
245 JmapGetOptions::default(),
246 )
247 .unwrap()
248 }
249
250 fn build_http_reply(status: u16, body: &[u8]) -> Vec<u8> {
251 let head = format!(
252 "HTTP/1.1 {} OK\r\nContent-Length: {}\r\nContent-Type: application/json\r\n\r\n",
253 status,
254 body.len()
255 );
256 let mut bytes = head.into_bytes();
257 bytes.extend_from_slice(body);
258 bytes
259 }
260
261 #[test]
262 fn success_returns_ok() {
263 let mut cor = make_get();
264 expect_wants_write(&mut cor, None);
265 expect_wants_read(&mut cor);
266
267 let body = br#"{
268 "methodResponses": [["Mailbox/get", {"list":[{"id":"m1"}],"notFound":[],"state":"s1"}, "c0"]],
269 "sessionState": "s1"
270 }"#;
271 let reply = build_http_reply(200, body);
272 let out = expect_complete_ok(&mut cor, &reply);
273 assert_eq!(out.list, vec![Probe { id: "m1".into() }]);
274 assert_eq!(out.state, "s1");
275 }
276
277 #[test]
278 fn method_error_returns_method_error() {
279 let mut cor = make_get();
280 expect_wants_write(&mut cor, None);
281 expect_wants_read(&mut cor);
282
283 let body = br#"{
284 "methodResponses": [["error", {"type":"accountNotFound"}, "c0"]],
285 "sessionState": "s1"
286 }"#;
287 let reply = build_http_reply(200, body);
288 let err = expect_complete_err(&mut cor, &reply);
289 assert!(matches!(err, JmapGetError::Method(_)));
290 }
291
292 #[test]
293 fn missing_response_returns_missing_response() {
294 let mut cor = make_get();
295 expect_wants_write(&mut cor, None);
296 expect_wants_read(&mut cor);
297
298 let body = br#"{"methodResponses": [], "sessionState": "s1"}"#;
299 let reply = build_http_reply(200, body);
300 let err = expect_complete_err(&mut cor, &reply);
301 assert!(matches!(err, JmapGetError::MissingResponse));
302 }
303
304 #[test]
305 fn parse_error_returns_parse_response() {
306 let mut cor = make_get();
307 expect_wants_write(&mut cor, None);
308 expect_wants_read(&mut cor);
309
310 let body = br#"{
311 "methodResponses": [["Mailbox/get", {"list":"nope"}, "c0"]],
312 "sessionState": "s1"
313 }"#;
314 let reply = build_http_reply(200, body);
315 let err = expect_complete_err(&mut cor, &reply);
316 assert!(matches!(err, JmapGetError::ParseResponse(_)));
317 }
318
319 #[test]
320 fn http_error_surfaces_as_send_error() {
321 let mut cor = make_get();
322 expect_wants_write(&mut cor, None);
323 expect_wants_read(&mut cor);
324
325 let reply = b"HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n";
326 let err = expect_complete_err(&mut cor, reply);
327 assert!(matches!(
328 err,
329 JmapGetError::Send(JmapSendError::HttpStatus(401))
330 ));
331 }
332
333 fn expect_wants_write(cor: &mut JmapGet<Probe>, arg: Option<&[u8]>) -> Vec<u8> {
336 match cor.resume(arg) {
337 JmapCoroutineState::Yielded(JmapYield::WantsWrite(bytes)) => bytes,
338 state => panic!("expected WantsWrite, got {state:?}"),
339 }
340 }
341
342 fn expect_wants_read(cor: &mut JmapGet<Probe>) {
343 match cor.resume(None) {
344 JmapCoroutineState::Yielded(JmapYield::WantsRead) => {}
345 state => panic!("expected WantsRead, got {state:?}"),
346 }
347 }
348
349 fn expect_complete_ok(cor: &mut JmapGet<Probe>, reply: &[u8]) -> JmapGetOutput<Probe> {
350 match cor.resume(Some(reply)) {
351 JmapCoroutineState::Complete(Ok(out)) => out,
352 state => panic!("expected Complete(Ok), got {state:?}"),
353 }
354 }
355
356 fn expect_complete_err(cor: &mut JmapGet<Probe>, reply: &[u8]) -> JmapGetError {
357 match cor.resume(Some(reply)) {
358 JmapCoroutineState::Complete(Err(err)) => err,
359 state => panic!("expected Complete(Err), got {state:?}"),
360 }
361 }
362}