ruststream/lib.rs
1//! Rust core of the [`RustStream`](https://github.com/powersemmi/ruststream) messaging
2//! framework: broker-agnostic traits, message types, codecs, router runtime, and a
3//! conformance harness for broker authors.
4//!
5//! # A service
6//!
7//! A handler is an `async fn` over a decoded payload. The application object mounts it on a
8//! broker, and the attribute writes the `main`:
9//!
10//! ```
11//! # #[cfg(all(feature = "macros", feature = "memory", feature = "json"))]
12//! # mod demo {
13//! use ruststream::memory::prelude::*;
14//! use serde::Deserialize;
15//!
16//! #[derive(Deserialize)]
17//! struct Order {
18//! id: u64,
19//! }
20//!
21//! #[subscriber("orders")]
22//! async fn handle(order: &Order) -> HandlerOutcome {
23//! println!("got order {}", order.id);
24//! HandlerOutcome::ack()
25//! }
26//!
27//! #[ruststream::app]
28//! fn app() -> RustStream {
29//! RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
30//! b.include(handle);
31//! })
32//! }
33//! # }
34//! # fn main() {}
35//! ```
36//!
37//! `cargo run -- run` starts it. The broker here is the in-process one; a real broker is a
38//! crate of its own with the same shape, and its prelude replaces `memory::prelude`.
39//!
40//! # Where things are
41//!
42//! * [`runtime`]: everything a service is made of. Subscribers and what a handler may take,
43//! replies and `Out` slots, routers, the per-delivery context and the shared state, typed
44//! headers, lifecycle hooks, middleware, failure policies, running beside another server.
45//! * [`codec`]: how payload bytes become values and back, where a codec is chosen, and the byte
46//! lanes that need none.
47//! * [`memory`]: the in-process broker, a real broker and the reference implementation.
48//! * [`testing`]: testing the service's own app, in process or against a running broker.
49//! * [`asyncapi`]: the generated document. [`metrics`], [`logging`], [`otel`]: observability.
50//! * [`runtime::cli`]: the generated entry point and the `ruststream` command.
51//! * [`conformance`]: the contract suite a broker crate runs against itself.
52//!
53//! The broker crates document their own descriptors and publish policies. Installation, the
54//! tutorial and the list of brokers are on the site:
55//! <https://powersemmi.github.io/ruststream/>.
56//!
57//! # Cargo features
58//!
59//! The core traits, the [`runtime::RustStream`] application object, middleware, and dispatch are
60//! always present. The rest is additive and opt-in. Codec features are mutually compatible and
61//! enable only the deserializers you need.
62//!
63//! * `json` (default): [`codec::JsonCodec`].
64//! * `msgpack`: [`codec::MsgpackCodec`].
65//! * `cbor`: [`codec::CborCodec`].
66//! * `memory`: [`memory::MemoryBroker`], an in-process broker usable in applications, prototypes
67//! and tests.
68//! * `macros`: the `#[subscriber]`, [`#[ruststream::app]`](macro@app),
69//! [`#[derive(Outgoing)]`](macro@Outgoing) and [`#[derive(MessageInfo)]`](macro@MessageInfo)
70//! macros.
71//! * `asyncapi`: `AsyncAPI` document generation and the HTML viewer.
72//! * `metrics`: Prometheus metrics middleware and exporter.
73//! * `logging`: colored, `RUST_LOG`-driven console logging via `tracing-subscriber`
74//! ([`logging::init`]). The generated `cli` `run` command installs it automatically.
75//! * `otel`: OpenTelemetry SDK integration: OTLP export for traces and metrics via
76//! [`otel::OtelBuilder::init`], plus per-handler dispatch metrics middleware and W3C
77//! trace-context propagation ([`otel::propagation`]).
78//! * `conformance`: the [`conformance::harness`] contract suite, per-capability suites in
79//! [`conformance::capabilities`], the in-process transport held to the server in
80//! [`conformance::in_process`], the checks of what a message carries in
81//! [`conformance::message_shape`], and broker-agnostic [`conformance::helpers`] for application
82//! tests. Generic over any broker's [`testing::TestableBroker`], so it pulls in no concrete broker
83//! (enable `memory` too to run it against [`memory::MemoryBroker`]).
84//! * `testing`: the [`testing::TestApp`] harness, which runs the service's production app in
85//! process or against running brokers.
86//! * `cli`: the `ruststream` binary (`run`, `asyncapi gen`, `new`).
87//! * `poll-diagnostics`: every subscription samples the time its handler spends in `poll` and
88//! warns when a handler computes on the app's runtime ([`runtime::PollDiagnostics`]).
89//!
90//! Disable defaults (`default-features = false`) to drop the bundled JSON codec; the core traits,
91//! runtime, and dispatch remain. Add back only what you need.
92//!
93//! # Getting the imports
94//!
95//! [`prelude`] carries what a service writes every time (the application object, the handler
96//! surface, the extractor parameters, the macros): `use ruststream::prelude::*;`. The broker and
97//! the codec stay explicit, since those are the choices a service makes for itself.
98
99#![forbid(unsafe_code)]
100
101mod broker;
102mod buffered;
103mod capability;
104mod describe;
105mod error;
106mod field;
107mod headers;
108mod message;
109pub mod prelude;
110mod publisher;
111mod schema;
112mod subscriber;
113mod subscription;
114pub mod testing;
115#[cfg(test)]
116mod testkit;
117mod typed_headers;
118
119/// Re-exported for the [`register_testable_broker!`] macro's expansion; not a stable API.
120#[cfg(feature = "testing")]
121#[doc(hidden)]
122pub use inventory;
123
124/// The buffer the crate's byte-producing surfaces speak: what a [`Codec`](codec::Codec) returns,
125/// what a [`Serialized`](runtime::Serialized) value writes into, and the payload a publisher
126/// that declared [`Take`] is handed.
127///
128/// Re-exported so that implementing either trait, or taking a published payload, does not also
129/// mean depending on `bytes` directly; a service that already does keeps its own import, which
130/// shadows this one.
131pub use bytes::BytesMut;
132
133/// The shared byte buffer the message path hands around: a payload, a header value taken with
134/// [`HeaderMap::get_shared`].
135///
136/// Re-exported for the same reason as [`BytesMut`]: reading a value out of a header map, or
137/// handing one to a publisher, should not also mean depending on `bytes` directly.
138pub use bytes::Bytes;
139
140/// The owned string of the message path: a destination name, a header key.
141///
142/// A `Bytes` that is valid UTF-8, so it reads as a `str`, clones by reference count and can be
143/// taken as a slice of a buffer the broker already holds. `Str::from_static("orders")` costs
144/// nothing at all; `String` moves in without a copy; `&str` copies once.
145pub use bytes_utils::Str;
146
147pub use broker::{Broker, Connected, ConnectedBroker};
148pub use buffered::{Buffered, BufferedSubscriber};
149pub use capability::{
150 ApiKeyLocation, BatchSubscriber, DescribeServer, HttpApiKeyLocation, OwnedTransactions,
151 Partitioned, Positioned, RequestReply, SecurityScheme, Seekable, Seeker, ServerSpec, Subscribe,
152 Transaction, TransactionalPublisher,
153};
154pub use describe::{AppId, AppIdError, Contact, ExternalDocs, License, Tag};
155pub use error::{AckError, DeclareRetryError};
156pub use field::{BuildBatchContext, BuildContext, ContextField, Field, FieldMut};
157pub use headers::HeaderMap;
158pub use message::{IncomingMessage, OutgoingMessage, RawMessage};
159pub use publisher::{
160 DefaultPublish, Lend, OutgoingFor, PairError, PayloadForm, PublishPolicy, Publisher, Take,
161};
162pub use schema::{
163 CallerName, DestinationForm, FixedName, HeadersContract, MessageHeaders, MessageInfo,
164 NameTemplate, NoHeaders, OutgoingDestination, WithHeaders,
165};
166pub use subscriber::Subscriber;
167pub use subscription::{
168 AddressedCopies, BrokerMoves, CopyPath, FromName, Name, NamedCopies, RedeliveryAddress,
169 RedeliveryAddressed, RetryDeclaration, StartAt, SubscriptionSource, Unnamed,
170};
171pub use typed_headers::{DeserializeHeadersError, SerializeHeadersError};
172
173pub mod codec;
174
175#[cfg(feature = "memory")]
176pub mod memory;
177
178pub mod runtime;
179
180pub use runtime::RustStream;
181
182/// Attribute macro that turns an `async fn` into a mountable subscriber definition.
183///
184/// Available with the `macros` feature. See [`ruststream_macros::subscriber`].
185#[cfg(feature = "macros")]
186pub use ruststream_macros::subscriber;
187
188/// Attribute macro that generates a `main` entry point from a `RustStream` builder function.
189///
190/// Available with the `macros` feature. See [`ruststream_macros::app`] and
191/// [`runtime::cli`].
192#[cfg(feature = "macros")]
193pub use ruststream_macros::app;
194
195/// Derive macro for [`MessageInfo`] metadata (type name + doc description).
196///
197/// Available with the `macros` feature.
198#[cfg(feature = "macros")]
199pub use ruststream_macros::MessageInfo;
200
201/// Derive macro declaring everything a message type says about being sent: its destination
202/// ([`OutgoingDestination`]) and its optional header contract ([`MessageHeaders`]), plus the
203/// [`MessageInfo`] metadata the generated document reads.
204///
205/// Available with the `macros` feature.
206#[cfg(feature = "macros")]
207pub use ruststream_macros::Outgoing;
208
209/// Derive macro that implements [`FromRef`](runtime::FromRef) for each field of an application
210/// state, so handlers can inject any field with [`State<T>`](runtime::State).
211///
212/// Available with the `macros` feature.
213#[cfg(feature = "macros")]
214pub use ruststream_macros::FromRef;
215
216/// Derive macro that makes a unit struct a slot marker ([`OutSlot`]) for
217/// `Out<impl Publisher, Marker>` handler parameters.
218///
219/// Available with the `macros` feature.
220#[cfg(feature = "macros")]
221pub use ruststream_macros::OutSlot;
222
223/// Derive macro for a declared message set: an enum whose variants each wrap one message
224/// model, named as the third `Out` argument (`Out<impl Publisher, Marker, SendSet>`). The enum
225/// is a type-level declaration and is never constructed.
226///
227/// Available with the `macros` feature.
228#[cfg(feature = "macros")]
229pub use ruststream_macros::OutMessages;
230
231/// Derive macro for a self-deserializing input type
232/// ([`Deserialized`](runtime::Deserialized)): a newtype or single-field struct over `&'a [u8]`
233/// gains the construction and the [`Input`](runtime::Input) spelling; the batch spelling
234/// (`&[Frame<'_>]`) follows from it.
235///
236/// Available with the `macros` feature.
237#[cfg(feature = "macros")]
238pub use ruststream_macros::Deserialized;
239
240/// Derive macro for a self-serialized outgoing type ([`Serialized`](runtime::Serialized)): a
241/// newtype or single-field struct over a byte buffer gains the bytes accessor and the wire
242/// spellings that route it onto the serialized wire ([`MessageWire`](runtime::MessageWire) for
243/// a typed publish, [`ReplyShape`](runtime::ReplyShape) for the reply position).
244///
245/// Available with the `macros` feature.
246#[cfg(feature = "macros")]
247pub use ruststream_macros::Serialized;
248
249// The trait shares the derive's name at the root (the serde pattern), so one import serves
250// both the `#[derive(OutSlot)]` and a broker's blanket impl bound.
251pub use runtime::OutSlot;
252
253#[cfg(feature = "conformance")]
254pub mod conformance;
255
256#[cfg(feature = "asyncapi")]
257pub mod asyncapi;
258
259/// Re-export of [`schemars`] so message types can derive `JsonSchema` without a direct dependency.
260///
261/// Derive it on a message type (`#[derive(ruststream::schemars::JsonSchema)]`) and its payload
262/// schema is emitted into the generated [`AsyncAPI`](asyncapi) document. Available with the
263/// `asyncapi` feature.
264#[cfg(feature = "asyncapi")]
265pub use schemars;
266
267#[cfg(feature = "metrics")]
268pub mod metrics;
269
270#[cfg(feature = "logging")]
271pub mod logging;
272
273#[cfg(feature = "otel")]
274pub mod otel;
275
276/// Implementation detail used by the `#[subscriber]` macro to capture a payload's JSON Schema.
277///
278/// Not part of the public API; no stability guarantees.
279#[doc(hidden)]
280pub mod __private {
281 use core::marker::PhantomData;
282
283 /// A type-carrying probe the macro reads a payload schema off.
284 ///
285 /// Schema selection uses inherent-vs-trait specialization: the schema
286 /// path is an inherent method on `Probe<T>` bounded by `T: JsonSchema`, and
287 /// [`NoSchemaProbe::schema_json`] is the trait fallback. Inherent methods win when present, so
288 /// `Probe::<T>::new().schema_json()` returns the schema for a concrete `T: JsonSchema` and
289 /// `None` otherwise - without forcing the bound onto every message type. The inherent method
290 /// exists only with the `asyncapi` feature.
291 #[derive(Debug)]
292 pub struct Probe<T>(pub PhantomData<T>);
293
294 impl<T> Probe<T> {
295 /// Constructs a probe for `T`.
296 #[must_use]
297 pub const fn new() -> Self {
298 Self(PhantomData)
299 }
300 }
301
302 impl<T> Default for Probe<T> {
303 fn default() -> Self {
304 Self::new()
305 }
306 }
307
308 /// The trait fallback: chosen for any `T` the inherent schema method does not cover.
309 pub trait NoSchemaProbe {
310 /// Returns `None` (no schema available for the probed type).
311 fn schema_json(&self) -> Option<String>;
312 }
313
314 impl<T> NoSchemaProbe for Probe<T> {
315 fn schema_json(&self) -> Option<String> {
316 None
317 }
318 }
319
320 #[cfg(feature = "asyncapi")]
321 impl<T: schemars::JsonSchema> Probe<T> {
322 /// Returns the serialized JSON Schema for `T` (inherent; preferred over the trait fallback).
323 #[must_use]
324 pub fn schema_json(&self) -> Option<String> {
325 crate::asyncapi::schema_json::<T>()
326 }
327 }
328
329 /// The trait fallback for the serialized-wire marker: chosen for any `T` that does not
330 /// implement [`Serialized`](crate::runtime::Serialized).
331 pub trait NoSerializedProbe {
332 /// Returns `false` (the probed type encodes through a codec, or is no message at all).
333 fn serialized_wire(&self) -> bool;
334 }
335
336 impl<T> NoSerializedProbe for Probe<T> {
337 fn serialized_wire(&self) -> bool {
338 false
339 }
340 }
341
342 impl<T: crate::runtime::Serialized> Probe<T> {
343 /// Returns `true`: the probed type carries its own wire bytes (inherent; preferred
344 /// over the trait fallback).
345 #[must_use]
346 pub fn serialized_wire(&self) -> bool {
347 true
348 }
349 }
350
351 /// The trait fallback for [`Message`](crate::MessageInfo) metadata: chosen for any `T` the
352 /// inherent methods below do not cover.
353 pub trait NoMessageProbe {
354 /// Returns `None` (the probed type does not implement `MessageInfo`).
355 fn message_name(&self) -> Option<&'static str>;
356 /// Returns `None` (the probed type does not implement `MessageInfo`).
357 fn message_description(&self) -> Option<&'static str>;
358 }
359
360 impl<T> NoMessageProbe for Probe<T> {
361 fn message_name(&self) -> Option<&'static str> {
362 None
363 }
364
365 fn message_description(&self) -> Option<&'static str> {
366 None
367 }
368 }
369
370 impl<T: crate::MessageInfo> Probe<T> {
371 /// Returns [`Message::NAME`](crate::MessageInfo::NAME) for `T` (inherent; preferred over the
372 /// trait fallback).
373 #[must_use]
374 pub fn message_name(&self) -> Option<&'static str> {
375 Some(T::NAME)
376 }
377
378 /// Returns [`Message::DESCRIPTION`](crate::MessageInfo::DESCRIPTION) for `T` (inherent;
379 /// preferred over the trait fallback).
380 #[must_use]
381 pub fn message_description(&self) -> Option<&'static str> {
382 T::DESCRIPTION
383 }
384 }
385
386 /// The trait fallback for header-contract schemas.
387 ///
388 /// Chosen for any `T` the inherent method below does not cover: no
389 /// [`MessageHeaders`](crate::MessageHeaders) impl, a [`NoHeaders`](crate::NoHeaders)
390 /// contract, or a contract type without a schema.
391 pub trait NoHeadersSchemaProbe {
392 /// Returns `None` (no headers schema available for the probed type).
393 fn headers_schema_json(&self) -> Option<String>;
394 }
395
396 impl<T> NoHeadersSchemaProbe for Probe<T> {
397 fn headers_schema_json(&self) -> Option<String> {
398 None
399 }
400 }
401
402 /// Renders a [`HeadersContract`](crate::HeadersContract) shape as a schema:
403 /// [`WithHeaders<H>`](crate::WithHeaders) yields `H`'s JSON Schema.
404 #[cfg(feature = "asyncapi")]
405 pub trait ContractSchema {
406 /// The serialized JSON Schema of the contract's header type, if any.
407 fn schema_json() -> Option<String>;
408 }
409
410 #[cfg(feature = "asyncapi")]
411 impl ContractSchema for crate::NoHeaders {
412 fn schema_json() -> Option<String> {
413 None
414 }
415 }
416
417 #[cfg(feature = "asyncapi")]
418 impl<H: schemars::JsonSchema> ContractSchema for crate::WithHeaders<H> {
419 fn schema_json() -> Option<String> {
420 crate::asyncapi::schema_json::<H>()
421 }
422 }
423
424 #[cfg(feature = "asyncapi")]
425 impl<T> Probe<T>
426 where
427 T: crate::MessageHeaders,
428 T::Contract: ContractSchema,
429 {
430 /// Returns the schema of `T`'s declared header contract (inherent; preferred over the
431 /// trait fallback).
432 #[must_use]
433 pub fn headers_schema_json(&self) -> Option<String> {
434 <T::Contract as ContractSchema>::schema_json()
435 }
436 }
437}
438
439/// Builds a [`NonZero`](core::num::NonZero) integer from a literal, rejecting zero at compile
440/// time.
441///
442/// The expansion is an inline `const` block, so `nonzero!(0)` fails the build instead of
443/// panicking at runtime, and the `NonZero` width is inferred from the call site - the same
444/// literal works for a batch size (`NonZeroUsize`) and any other `NonZero` parameter.
445///
446/// # Examples
447///
448/// ```
449/// # #[cfg(all(feature = "macros", feature = "memory", feature = "json"))]
450/// # mod demo {
451/// use ruststream::memory::prelude::*;
452/// use serde::Deserialize;
453///
454/// #[derive(Deserialize)]
455/// struct Order {
456/// id: u64,
457/// }
458///
459/// #[subscriber("orders")]
460/// async fn settle(orders: &[Order]) -> HandlerOutcome {
461/// tracing::info!(count = orders.len(), "settled a batch");
462/// HandlerOutcome::ack()
463/// }
464///
465/// fn app() -> RustStream {
466/// RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
467/// b.include(settle.batch(nonzero!(128)).workers(nonzero!(4)));
468/// })
469/// }
470/// # }
471/// # fn main() {}
472/// ```
473///
474/// Zero does not compile:
475///
476/// ```compile_fail
477/// let _: core::num::NonZeroUsize = ruststream::nonzero!(0);
478/// ```
479#[macro_export]
480macro_rules! nonzero {
481 ($value:expr) => {
482 const {
483 match ::core::num::NonZero::new($value) {
484 ::core::option::Option::Some(value) => value,
485 ::core::option::Option::None => panic!("nonzero!(..) requires a non-zero value"),
486 }
487 }
488 };
489}