AsyncAPI¶
With the asyncapi feature, RustStream generates an AsyncAPI 3.0
document from the application's handlers: each subscriber becomes a channel and a receive
operation, and payload types contribute schemas.
Generating the document¶
The quickest path is the CLI, which runs your service's generator and prints the document:
ruststream asyncapi gen # JSON to stdout
ruststream asyncapi gen -o asyncapi.json
ruststream asyncapi gen --yaml
In code, build the spec from the application with build_spec, then serialize it with to_json or
to_yaml:
/// Builds the AsyncAPI document and the viewer HTML from the service.
fn document() -> Result<(String, String), serde_json::Error> {
let spec = build_spec(&service()).to_json()?;
let viewer = render_viewer_html("/asyncapi.json", &ViewerOptions::default());
Ok((spec, viewer))
}
#[ruststream::app] wires the asyncapi gen command to build_spec for you, so the CLI and a
hand-written call produce the same document.
Payload schemas¶
A handler's payload type appears as a schema when it derives JsonSchema. RustStream re-exports
schemars, so you do not need a direct dependency:
/// An order placed by a customer.
#[derive(Debug, Deserialize, ruststream::Message, ruststream::schemars::JsonSchema)]
struct Order {
id: u64,
item: String,
}
A type without JsonSchema still works as a handler payload; it just contributes no schema to the
document.
Message names and descriptions¶
A documented payload type feeds the message component on its own: with the JsonSchema derive,
the type's doc comment becomes the message description, and a #[schemars(title = "...")] (or
rename) names the component. Without a schema, the component is named after the payload type and
the description falls back to the handler's doc comment (which also documents the receive
operation).
To control the metadata explicitly - including for types without JsonSchema - implement the
Message trait, which takes precedence over the schema; or derive it, which uses the type's name
and doc comment:
use ruststream::Message;
/// An order placed by a customer.
#[derive(Message, serde::Deserialize)]
struct Order {
id: u64,
}
// In the document: components.messages.Order with that description.
A manual impl Message can name the component differently from the Rust type
(const NAME: &'static str = "CustomOrder";), which keeps the wire contract stable across renames.
Servers¶
Record the servers your service connects to so they appear in the document's servers section.
Build a ServerSpec directly:
fn service() -> RustStream {
RustStream::new(AppInfo::new("orders", "0.1.0"))
.server(
"production",
ruststream::ServerSpec::new("nats.example.com:4222", "nats"),
)
.with_broker(MemoryBroker::new(), |b| b.include(handle))
}
A broker crate may also implement the DescribeServer capability, in which case
broker.describe_server() produces the spec for you (none of the shipped brokers do yet).
Serving the document¶
Hosting is intentionally not part of the framework. build_spec and to_json / to_yaml give you
the bytes; you mount them in whatever HTTP stack you already run (axum, actix, or any other).
For an interactive viewer, render_viewer_html returns a self-contained HTML page that loads the
AsyncAPI React component and points it at your spec URL:
use ruststream::asyncapi::{render_viewer_html, ViewerOptions};
let html = render_viewer_html("/asyncapi.json", &ViewerOptions::default());
Serve that HTML and the spec JSON from two routes in your own server. By default the viewer loads its
assets from a CDN; override the base URL with ViewerOptions::with_cdn_base for offline or
locked-down deployments (with_title sets the page title).
A complete server¶
The asyncapi_http
example serves the document and the viewer with axum. Run it with
cargo run --example asyncapi_http --features macros,memory,asyncapi, then open
http://127.0.0.1:8080/.
//! Serve a service's AsyncAPI document and an interactive viewer over HTTP with axum.
//!
//! ```text
//! cargo run --example asyncapi_http --features macros,memory,asyncapi
//! ```
//!
//! Then open <http://127.0.0.1:8080/> for the viewer, or fetch the raw document:
//!
//! ```text
//! curl http://127.0.0.1:8080/asyncapi.json
//! ```
use axum::Router;
use axum::http::header::CONTENT_TYPE;
use axum::response::{Html, IntoResponse};
use axum::routing::get;
use ruststream::asyncapi::{ViewerOptions, build_spec, render_viewer_html};
use ruststream::memory::MemoryBroker;
use ruststream::runtime::{AppInfo, HandlerResult, RustStream};
use ruststream::subscriber;
use serde::Deserialize;
/// An order placed by a customer.
#[derive(Debug, Deserialize, ruststream::Message, ruststream::schemars::JsonSchema)]
struct Order {
id: u64,
item: String,
}
#[subscriber("orders")]
async fn handle(order: &Order) -> HandlerResult {
println!("order {} ({})", order.id, order.item);
HandlerResult::Ack
}
fn service() -> RustStream {
RustStream::new(AppInfo::new("orders", "0.1.0"))
.server(
"production",
ruststream::ServerSpec::new("nats.example.com:4222", "nats"),
)
.with_broker(MemoryBroker::new(), |b| b.include(handle))
}
/// Builds the AsyncAPI document and the viewer HTML from the service.
fn document() -> Result<(String, String), serde_json::Error> {
let spec = build_spec(&service()).to_json()?;
let viewer = render_viewer_html("/asyncapi.json", &ViewerOptions::default());
Ok((spec, viewer))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let (spec, viewer) = document()?;
let router = Router::new()
.route(
"/",
get(move || {
let viewer = viewer.clone();
async move { Html(viewer) }
}),
)
.route(
"/asyncapi.json",
get(move || {
let spec = spec.clone();
async move { ([(CONTENT_TYPE, "application/json")], spec).into_response() }
}),
);
let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await?;
println!("AsyncAPI viewer on http://127.0.0.1:8080/");
axum::serve(listener, router).await?;
Ok(())
}