Skip to main content

nprpc/
lib.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! # Not Postcard-RPC
6//!
7//! It needs a better name. `nprpc` is a tool for communication that is:
8//!
9//! * Strongly typed (and Rust focused)
10//!     * Uses `postcard-schema` for deriving payload schemas.
11//!     * Messages are tagged with an eight-byte hash of the method name,
12//!       request schema, and response schema. Hash collisions are caught at
13//!       compile time, and Cool Schemas Never Change.
14//!     * Uses `serde` and `postcard` are used for serialization and
15//!       deserialization.
16//!     * `postcard-dyn` can be used for dynamic ser/de, and transcoding frames
17//!       to/from JSON (Coming Soon™️).
18//! * Strongly ordered
19//!     * Client initiates request to Server, then Server replies to Client.
20//!     * No concurrent in-flight requests.
21//!     * No multi-part requests or responses.
22//!     * Currently only blocking client/servers supported.
23//!     * At least an async (Tokio) client Coming Soon™️.
24//! * Flexible transport and storage
25//!     * Can be used with or without a heap, both for frame buffering as well
26//!       as request/response data payloads.
27//!     * Can work over any transport, clients and servers both have `Io` traits
28//!       for custom plumbing.
29//!     * For bounded types, maximum message sizes can be calculated, and
30//!       necessary buffer sizes are generated as `const`s.
31//! * Support for flexible data payloads:
32//!     * Variable length messages supported.
33//!     * `&[u8]` or `&str`s can be borrowed from the incoming frame (for
34//!       requests), or borrowed from the server for the outgoing frame (for
35//!       responses).
36//!     * Support for "type punning" on either side of the connection, e.g. the
37//!       server can send a `&str` and the client can receive an owned `String`.
38//! * Doesn't concern itself with reliable delivery (and doesn't attempt to add
39//!   reliability, That's On You).
40//!
41//! ## Lightning Tour
42//!
43//! We call "a set of remotely callable methods" an "interface". You define
44//! an interface using the [`interface!`] macro. You usually do this in a shared
45//! `example-api` crate.
46//!
47//! ```rust
48//! use nprpc::interface;
49//!
50//! interface! {
51//!     /// This module named "example" contains everything you need for both
52//!     /// the client and server.
53//!     mod example {
54//!         /// We support doc comments and `#[cfg]`s on methods. Methods are
55//!         /// written using a mostly-rust-like syntax.
56//!         fn echo(u32) -> u32;
57//!     }
58//! }
59//! ```
60//!
61//! In order to implement the server for this interface, you need to implement
62//! the `example::Server` trait's methods. You can use rust-analyzer's
63//! "Implement missing methods" action to do this quickly.
64//!
65//! ```rust
66//! # use nprpc::interface;
67//! #
68//! # interface! {
69//! #     /// This module named "example" contains everything you need for both
70//! #     /// the client and server.
71//! #     mod example {
72//! #         /// We support doc comments and `#[cfg]`s on methods. Methods are
73//! #         /// written using a mostly-rust-like syntax.
74//! #         fn echo(u32) -> u32;
75//! #     }
76//! # }
77//! use nprpc::Request;
78//! // From the `example-api` crate above
79//! use example::Server;
80//!
81//! // Not shown: impl Backend for WireBackend { .. }
82//! struct WireBackend {
83//!     // ...
84//! #   a: WireStorage,
85//! #   b: WireIo,
86//! }
87//!
88//! // Server-specific type that contains necessary trait
89//! struct ServerImpl {
90//!     // ...
91//! }
92//! # impl ServerImpl { pub fn new() -> Self { Self {} }}
93//!
94//! // Implementation of `example` defined by `interface!` above
95//! impl example::Server for ServerImpl {
96//!     fn echo(&mut self, req: Request<u32>) -> u32 {
97//!         // `req` contains both the header of the request as well as the
98//!         // body, what we defined in the interface! macro. We just copy the
99//!         // data back out.
100//!         *req.body
101//!     }
102//! }
103//!
104//! # struct WireIo;
105//! # struct WireStorage;
106//! # use nprpc::io::server::{ServerIoError, RawIoFrame};
107//! # impl nprpc::io::server::Io for WireIo {
108//! #     type Error = ();
109//! #     type Meta = ();
110//! #     fn recv_one_frame_raw<'data>(&mut self, _: &'data mut [u8])
111//! #         -> Result<Option<RawIoFrame<'data, ()>>, ServerIoError<()>>
112//! #     {
113//! #         Err(nprpc::io::server::ServerIoError::Io(()))
114//! #     }
115//! #     fn send_one_frame_raw(&mut self, _: RawIoFrame<'_, ()>)
116//! #         -> Result<(), ServerIoError<()>> { todo!() }
117//! # }
118//! # impl nprpc::io::Storage for WireStorage {
119//! #     fn buffers(&mut self) -> nprpc::io::StorageView<'_> {
120//! #         nprpc::io::StorageView { rqst_buf: &mut [], resp_buf: &mut [] }
121//! #     }
122//! # }
123//! # impl nprpc::io::server::Backend for WireBackend {
124//! #     type Io = WireIo;
125//! #     type Storage = WireStorage;
126//! #     fn parts(&mut self) -> (&mut WireStorage, &mut WireIo) {
127//! #         let Self { a, b } = self;
128//! #         (a, b)
129//! #     }
130//! # }
131//! #
132//! # impl WireBackend {
133//! #     fn new() -> Self { Self { a: WireStorage, b: WireIo }}
134//! # }
135//! #
136//! fn main() {
137//!     let mut wire = WireBackend::new();
138//!     let mut server = ServerImpl::new();
139//!
140//!     // Server one request, typically done in a loop
141//!     let res = server.serve_one(&mut wire);
142//!     if let Err(e) = res {
143//!         println!("Err: {e:?}");
144//!     }
145//! }
146//!
147//! ```
148//!
149//! As a client, you'll get an extension trait called `Client` that lets you
150//! call the methods you defined.
151//!
152//! ```rust,no_run
153//! # use nprpc::interface;
154//! #
155//! # interface! {
156//! #     /// This module named "example" contains everything you need for both
157//! #     /// the client and server.
158//! #     mod example {
159//! #         /// We support doc comments and `#[cfg]`s on methods. Methods are
160//! #         /// written using a mostly-rust-like syntax.
161//! #         fn echo(u32) -> u32;
162//! #     }
163//! # }
164//! #
165//! # struct WireIo;
166//! # struct WireStorage;
167//! # impl nprpc::io::client::Io for WireIo {
168//! #     type Error = ();
169//! #     fn send_then_receive_raw_frames<'a>(&mut self, _: &[u8], _: &'a mut [u8])
170//! #         -> Result<&'a [u8], ()> { todo!() }
171//! # }
172//! # impl nprpc::io::Storage for WireStorage {
173//! #     fn buffers(&mut self) -> nprpc::io::StorageView<'_> { todo!() }
174//! # }
175//! # impl nprpc::io::client::Backend for WireBackend {
176//! #     type Io = WireIo;
177//! #     type Storage = WireStorage;
178//! #     fn next_sequence_number(&mut self) -> u16 { todo!() }
179//! #     fn parts(&mut self) -> (&mut WireStorage, &mut WireIo) { todo!() }
180//! # }
181//! // Not shown: impl Backend for WireBackend { .. }
182//! struct WireBackend;
183//!
184//! use nprpc::io::client::ClientIoError;
185//! use nprpc::Response;
186//!
187//! // Pull in the extension trait to get access to the methods for any type
188//! // that impls `Backend`
189//! use example::Client;
190//!
191//! fn main() {
192//!     let mut backend = WireBackend;
193//!     let res: Result<Response<u32>, ClientIoError<_>> = backend.echo(&123);
194//!     # drop(res);
195//! }
196//!
197//! ```
198//!
199//! ## Naming Guide
200//!
201//! For the sake of consistency, we use the following naming scheme for types
202//! and lifetimes. Prefer the 4-letter abbreviations to keep pieces aligned in
203//! multi-line code.
204//!
205//! * "Request"
206//!      * A message sent by a client, and received by a server
207//!      * `request`, `rqst`, or `'rqst`
208//! * "Response"
209//!      * A message sent by a server, and received by a client
210//!      * `response`, `resp`, or `'resp`
211//! * "Header"
212//!      * Included in every request and response, same type in all items
213//!      * `header` or `hedr`
214//! * "Body"
215//!      * The type-specific payload. Used for both the serialized form as well
216//!        as the rust-type form.
217//!      * Always `body`. May have the lifetime `'rqst` or `'resp`.
218
219#![cfg_attr(not(feature = "std"), no_std)]
220
221pub mod macros;
222
223pub mod interface;
224pub mod io;
225pub mod wire;
226
227pub use io::client::{ClientError, ClientIoError};
228pub use io::server::{ServerError, ServerIoError};
229
230// TODO: Should the `Key`s we generate also hash the Header and Error type to
231// ensure complete compatibility? Do we consider this part of the versioning?
232
233/// Not covered by semver, re-exported items for external macros
234#[doc(hidden)]
235pub mod __private {
236    pub use postcard_schema_ng::Schema;
237    pub use postcard_schema_ng::key::Key;
238    pub use postcard_schema_ng::schema::DataModelType;
239    pub use serde::{Deserialize, Serialize};
240}
241
242/// Borrowed view of a request
243///
244/// Contains a reference to a request header and request body.
245#[derive(Debug, Clone, Copy)]
246pub struct Request<'rqst, T> {
247    pub hedr: &'rqst wire::Header,
248    pub body: &'rqst T,
249}
250
251/// Borrowed view of a request
252///
253/// Like `Request`, but the body has not been deserialized.
254#[derive(Debug, Clone, Copy)]
255pub struct RequestRaw<'rqst> {
256    pub hedr: &'rqst wire::Header,
257    pub body: &'rqst [u8],
258}
259
260/// Owned view of a response
261#[derive(Debug)]
262pub struct Response<U> {
263    pub hedr: wire::Header,
264    pub body: U,
265}