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