io_msgraph/lib.rs
1#![no_std]
2#![deny(missing_docs)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4
5//! # io-msgraph
6//!
7//! I/O-free coroutines for the [Microsoft Graph API], built on
8//! [io-http] (HTTP/1.1) and pumped by any stream the caller owns.
9//!
10//! [Microsoft Graph API]: https://learn.microsoft.com/en-us/graph/api/overview
11//! [io-http]: https://docs.rs/io-http
12//!
13//! io-msgraph is the Microsoft Graph sibling of [io-gmail]: same shape
14//! (JSON over HTTP), different vendor. Unlike io-gmail, which is scoped
15//! to Gmail's mail API, io-msgraph represents the whole Microsoft Graph
16//! API: the mail and contacts surfaces are covered today, and sibling
17//! resources (calendars among others) will be added under the same tree
18//! over time.
19//!
20//! [io-gmail]: https://docs.rs/io-gmail
21//!
22//! ## Layers and features
23//!
24//! The crate has two of the three standard Pimalaya layers; there is no
25//! CLI. The always-present no_std core holds the I/O-free coroutines,
26//! the whole Microsoft Graph REST logic. The `client` feature adds
27//! [`v1::client::MsgraphClientStd`], a std blocking client over any
28//! stream, whose `connect` constructor opens the TCP/TLS connection
29//! itself behind a TLS feature (`rustls-ring` by default, `rustls-aws`,
30//! `native-tls`).
31//!
32//! ## Everything lives under v1
33//!
34//! The Microsoft Graph API is versioned (`/v1.0/`), so the crate is
35//! too: the version-agnostic [`coroutine`] contract stays at the crate
36//! root, everything else lives under [`v1`]. The day a breaking Graph
37//! version ships, a sibling module slots in without breaking `v1`
38//! consumers.
39//!
40//! ## The coroutine contract
41//!
42//! Every exchange implements [`coroutine::MsgraphCoroutine`]: `resume`
43//! takes the bytes read since the last yield and either requests I/O
44//! ([`coroutine::MsgraphYield`] `WantsRead` / `WantsWrite`) or
45//! completes. The [`msgraph_try!`] macro is the coroutine equivalent
46//! of `?`.
47//!
48//! A Graph call is a single HTTP request/response, so every REST
49//! coroutine is a thin wrapper around one shared primitive,
50//! [`v1::send::MsgraphSend`]: it builds the authorized request (bearer
51//! token, JSON in and out) and parses either the 2xx body or Graph's
52//! error envelope into [`v1::send::MsgraphSendError`]. Redirects are
53//! never followed. The terminal [`v1::send::MsgraphSendOutput`] carries
54//! the parsed response plus a keep-alive flag so pumps can reuse the
55//! connection across the many small requests a Graph session makes.
56//! Empty 2xx bodies (DELETE, sendMail, send draft) deserialize into the
57//! [`v1::send::MsgraphNoResponse`] unit marker. The two `$value`
58//! endpoints (raw message MIME and raw attachment content) return bytes
59//! rather than JSON: they run the HTTP send directly and yield the
60//! response body.
61//!
62//! ## Naming
63//!
64//! Public items follow `<Domain><Target><Verb><Ext>`: the domain is
65//! `Msgraph`, the target-verb pair mirrors the REST operation
66//! (`MsgraphMailFolderCreate` for creating a mail folder,
67//! `MsgraphContactsList` for listing contacts) and the extension
68//! distinguishes companions (`Params`, `Response`, `Error`, `Yield`).
69//! Pure data resources omit the verb (`MsgraphMessage`,
70//! `MsgraphContact`); the target is omitted when the verb applies to
71//! the whole exchange ([`v1::send::MsgraphSend`]).
72//!
73//! ## Module layout
74//!
75//! [`v1::rest`] mirrors the Graph reference. The mail and contacts
76//! surfaces hang off the users resource, so they live under
77//! [`v1::rest::users`], with each sub-resource a directory and each
78//! operation a file named after it: mail_folders (list, get, create,
79//! update, delete, move, copy, child_folders), messages (list, get,
80//! get_raw, create, create_mime, update, delete, move, copy, send,
81//! attachments), contact_folders (list, get, create, update, delete,
82//! child_folders), contacts (list, get, create, update, delete, delta)
83//! and the sendMail action (JSON and MIME form). A reader who knows the
84//! reference knows where to look.
85//!
86//! Domain types mirror the Graph schema. Full-resource bodies double as
87//! create and update bodies thanks to `skip_serializing_if` on every
88//! optional field; contact fields use the [`v1::field::MsgraphField`]
89//! tri-state to distinguish a field left out of a PATCH body from one
90//! explicitly cleared. List operations take borrowed `*Params` structs
91//! whose fields rename to the OData system query options (`$top`,
92//! `$select`, `$filter` among others), flattened into query pairs by
93//! [`v1::query::to_query_pairs`], a tiny no_std serde serializer.
94//!
95//! ## Authentication
96//!
97//! io-msgraph does no OAuth itself: the Graph API only accepts OAuth
98//! 2.0 bearer tokens, so the credential is exactly a bare access token,
99//! and minting or refreshing it is the caller's responsibility. The
100//! base URL is fixed ([`v1::send::MSGRAPH_API_BASE`]); the mailbox
101//! owner is addressed by [`v1::send::user_path`], which yields `me` for
102//! the authenticated user or `users/{id}` for an explicit user id or
103//! principal name (Graph rejects `users/me`).
104//!
105//! ## Logging
106//!
107//! Coroutines pair a `debug!` lifecycle line with one `trace!` per
108//! input variable in `new()`, and a `debug!` plus `trace!("out: ...")`
109//! when `resume` completes; the crate never logs above `debug!`.
110//!
111//! ## Example
112//!
113//! Running a coroutine against a caller-owned TLS stream:
114//!
115//! ```rust,no_run
116//! use std::{
117//! io::{Read, Write},
118//! net::TcpStream,
119//! sync::Arc,
120//! };
121//!
122//! use io_http::rfc6750::bearer::HttpAuthBearer;
123//! use io_msgraph::{coroutine::*, v1::rest::users::get::MsgraphUserGet};
124//! use rustls::{ClientConfig, ClientConnection, StreamOwned};
125//! use rustls_platform_verifier::ConfigVerifierExt;
126//!
127//! let config = ClientConfig::with_platform_verifier().unwrap();
128//! let server_name = "graph.microsoft.com".try_into().unwrap();
129//! let conn = ClientConnection::new(Arc::new(config), server_name).unwrap();
130//! let tcp = TcpStream::connect(("graph.microsoft.com", 443)).unwrap();
131//! let mut stream = StreamOwned::new(conn, tcp);
132//!
133//! let auth = HttpAuthBearer::new("token");
134//! let mut coroutine = MsgraphUserGet::new(&auth, "me").unwrap();
135//!
136//! let mut arg: Option<&[u8]> = None;
137//! let mut buf = [0u8; 8192];
138//! let mut read = Vec::new();
139//!
140//! let out = loop {
141//! match coroutine.resume(arg.take()) {
142//! MsgraphCoroutineState::Complete(Ok(out)) => break out,
143//! MsgraphCoroutineState::Complete(Err(err)) => panic!("{err}"),
144//! MsgraphCoroutineState::Yielded(MsgraphYield::WantsRead) => {
145//! let n = stream.read(&mut buf).unwrap();
146//! read.clear();
147//! read.extend_from_slice(&buf[..n]);
148//! arg = Some(&read);
149//! }
150//! MsgraphCoroutineState::Yielded(MsgraphYield::WantsWrite(bytes)) => {
151//! stream.write_all(&bytes).unwrap();
152//! }
153//! }
154//! };
155//!
156//! println!("user principal name: {:?}", out.response.user_principal_name);
157//! ```
158
159extern crate alloc;
160#[cfg(feature = "client")]
161extern crate std;
162
163pub mod coroutine;
164pub mod v1;