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