Skip to main content

io_msgraph/v1/rest/
batch.rs

1//! Combine several Microsoft Graph requests into one HTTP call (`POST
2//! /$batch`).
3//!
4//! <https://learn.microsoft.com/en-us/graph/json-batching>
5
6use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
7
8use io_http::rfc6750::bearer::HttpAuthBearer;
9use log::{debug, trace};
10use serde::{Deserialize, Serialize, de::DeserializeOwned};
11use serde_json::Value;
12use url::Url;
13
14use crate::{
15    coroutine::*,
16    msgraph_try,
17    v1::send::{
18        MSGRAPH_API_BASE, MsgraphSend, MsgraphSendError, MsgraphSendOutput, parse_api_error,
19    },
20};
21
22/// Maximum number of requests Graph accepts in one batch.
23pub const MSGRAPH_BATCH_MAX_REQUESTS: usize = 20;
24
25/// One request of a batch.
26#[derive(Debug, Clone, Default, Deserialize, Serialize, Eq, PartialEq)]
27#[serde(rename_all = "camelCase")]
28pub struct MsgraphBatchRequest {
29    /// The id correlating the request with its response, unique within
30    /// the batch.
31    pub id: String,
32    /// The HTTP method.
33    pub method: String,
34    /// The URL relative to the API version, such as
35    /// `/me/mailFolders/inbox`.
36    pub url: String,
37    /// The request headers, `Content-Type` being required with a body.
38    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
39    pub headers: BTreeMap<String, String>,
40    /// The JSON request body.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub body: Option<Value>,
43    /// The ids of the requests this one runs after.
44    #[serde(default, skip_serializing_if = "Vec::is_empty")]
45    pub depends_on: Vec<String>,
46}
47
48/// One response of a batch.
49#[derive(Debug, Clone, Default, Deserialize, Serialize, Eq, PartialEq)]
50pub struct MsgraphBatchResponse {
51    /// The id of the request this response answers.
52    pub id: String,
53    /// The HTTP status of the response.
54    pub status: u16,
55    /// The response headers.
56    #[serde(default)]
57    pub headers: BTreeMap<String, String>,
58    /// The JSON response body, or the error envelope on failure.
59    #[serde(default)]
60    pub body: Option<Value>,
61}
62
63impl MsgraphBatchResponse {
64    /// Parses the body of a 2xx response into `T`, or turns any other
65    /// status into [`MsgraphSendError::Api`].
66    pub fn parse<T: DeserializeOwned>(self) -> Result<T, MsgraphSendError> {
67        let body = self.body.unwrap_or(Value::Null);
68
69        if (200..300).contains(&self.status) {
70            return serde_json::from_value(body).map_err(MsgraphSendError::ParseResponse);
71        }
72
73        let body = serde_json::to_vec(&body).unwrap_or_default();
74        let (status, code, message) = parse_api_error(self.status, &body);
75        Err(MsgraphSendError::Api {
76            status,
77            code,
78            message,
79        })
80    }
81}
82
83/// The batch response body, in no particular order.
84#[derive(Debug, Clone, Default, Deserialize, Serialize, Eq, PartialEq)]
85pub struct MsgraphBatchResponses {
86    /// The responses, one per request.
87    #[serde(default)]
88    pub responses: Vec<MsgraphBatchResponse>,
89}
90
91#[derive(Serialize)]
92struct Body<'a> {
93    requests: &'a [MsgraphBatchRequest],
94}
95
96/// Sends a Microsoft Graph batch.
97pub struct MsgraphBatch {
98    send: MsgraphSend<MsgraphBatchResponses>,
99}
100
101impl MsgraphBatch {
102    /// Sends `requests`, between 1 and [`MSGRAPH_BATCH_MAX_REQUESTS`]
103    /// with unique ids.
104    pub fn new(
105        auth: &HttpAuthBearer,
106        requests: &[MsgraphBatchRequest],
107    ) -> Result<Self, MsgraphSendError> {
108        debug!("prepare microsoft graph batch");
109        trace!("requests: {requests:?}");
110
111        if requests.is_empty() || requests.len() > MSGRAPH_BATCH_MAX_REQUESTS {
112            let err = MsgraphSendError::InvalidRequest(format!(
113                "Batch must hold between 1 and {MSGRAPH_BATCH_MAX_REQUESTS} requests"
114            ));
115            return Err(err);
116        }
117
118        for (i, request) in requests.iter().enumerate() {
119            if requests[..i].iter().any(|prev| prev.id == request.id) {
120                let err = MsgraphSendError::InvalidRequest(format!(
121                    "Batch request id `{}` is not unique",
122                    request.id
123                ));
124                return Err(err);
125            }
126        }
127
128        let url = Url::parse(MSGRAPH_API_BASE)?.join("$batch")?;
129        let send = MsgraphSend::post_json(auth, url, &Body { requests })?;
130
131        Ok(Self { send })
132    }
133}
134
135impl MsgraphCoroutine for MsgraphBatch {
136    type Yield = MsgraphYield;
137    type Return = Result<MsgraphSendOutput<MsgraphBatchResponses>, MsgraphSendError>;
138
139    fn resume(&mut self, arg: Option<&[u8]>) -> MsgraphCoroutineState<Self::Yield, Self::Return> {
140        let out = msgraph_try!(&mut self.send, arg);
141        debug!("batch sent");
142        trace!("out: {out:?}");
143        MsgraphCoroutineState::Complete(Ok(out))
144    }
145}