AsyncAPI documentation
Generate an AsyncAPI 3.1.0 document from handler metadata and read it through DI after startup.
Consumer can generate an immutable AsyncAPI 3.1.0 document from its accepted handler plan and effective topology. Its document service is attached to the DI container after successful startup. The framework creates no automatic HTTP route, file, UI or publisher operation; the application chooses how to expose the document.
Feature and schema type#
[dependencies]
lilyrs = { version = "0.1.0", features = ["consumer-asyncapi"] }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }Public configuration and model types are under lilyrs::consumer. Use that module’s schemars re-export for JsonSchema instead of adding an unrelated version. Queue-only queue-asyncapi metadata support does not install the Consumer document service by itself. There is no separate lilyrs::asyncapi facade module.
Complete handler and document-builder example#
Use this module in place of the first application’s handler. Set the queue name and routing_key to docs.api.events, and exchange_name to docs.api; retain the connection settings and six retention values. documented_builder() creates the AsyncAPI configuration, but does not generate RabbitMQ connection/topology configuration or a lily.toml file. Your main function or managed host runs the builder.
use lilyrs::consumer::{
AsyncApiBuildError, AsyncApiConfig, AsyncApiSecurityScheme, AsyncApiServer,
AsyncApiServerProtocol, AsyncApiTag, Consumer, ConsumerAsyncApiService,
ConsumerBuilder, Extensions, Injectable, ServiceTrait,
};
use lilyrs::consumer::schemars::{self, JsonSchema};
use lilyrs::queue::{Json, QueueHandlerError, asyncapi, queue, queue_service};
use serde::Deserialize;
#[derive(Deserialize, JsonSchema)]
struct ApiEvent { id: String }
#[derive(Injectable)]
#[service(lifetime = "Scoped")]
struct ApiWorker;
impl ServiceTrait for ApiWorker {}
#[queue_service]
#[asyncapi(tag = "orders", security = "consumer-auth")]
impl ApiWorker {
#[queue("docs.api.events", version = 1, content = "json")]
#[asyncapi(operation_id = "consumeApiEventV1", summary = "Apply one event", example = r#"{"id":"order-42"}"#)]
async fn apply(&self, Json(event): Json<ApiEvent>) -> Result<(), QueueHandlerError> {
if event.id.is_empty() {
return Err(QueueHandlerError::permanent("EVENT_INVALID"));
}
Ok(())
}
}
pub fn documented_builder() -> Result<ConsumerBuilder, AsyncApiBuildError> {
let mut document = AsyncApiConfig::new("Documentation consumer", "1.0.0")?;
document.add_tag(AsyncApiTag::new("orders")?)?;
document.add_security_scheme(AsyncApiSecurityScheme::user_password("consumer-auth")?)?;
document.add_server(AsyncApiServer::new(
"public-broker", "rabbitmq.example.com:5671", AsyncApiServerProtocol::Amqps,
)?)?;
Ok(Consumer::builder().asyncapi(document))
}
// Call after successful managed startup through the same caller-owned container.
pub async fn document_bytes(extensions: &Extensions) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let document = extensions.get_service::<ConsumerAsyncApiService>(None).await?;
Ok(document.canonical_json()?.to_vec())
}Download the complete asyncapi.rs example.
Metadata contract rules#
| Handler payload | Document contract |
|---|---|
Json<T> |
T: JsonSchema; the JSON schema is inferred. |
| Built-in text / binary | The built-in payload contract is inferred. |
| Raw, custom or body-unobserved handler | An explicit schema = Type, content_type = "..." or opaque, content_type = "..." contract is required. |
| Handler excluded from the document | Exclude it explicitly using #[asyncapi(skip)]. |
When document generation is enabled, every active handler needs a consistent documented or explicit-skip state. Missing metadata is not treated as a silently omitted endpoint. Do not combine schema with opaque or arbitrarily override the schema authority of a built-in payload. A disagreement between runtime and document contracts is a build error.
A service annotation supplies shared tag and security information, while a handler can set its own summary and description. A handler cannot make a service-level skipped item documented again. Duplicate operation IDs, undeclared tag/security references and a second .asyncapi(config) builder call are rejected.
When is the document available?#
ConsumerAsyncApiService is attached only after physical queue registration and the final startup cancellation fence succeed. Do not expect it to exist in an eager DI constructor. Call the example’s document_bytes after successful managed startup using Extensions from the same caller-owned container.
There is no public API for extracting a hidden container from the default owned Consumer’s ManagedConsumer handle. To serve the document from a parent HTTP application, arrange shared-container ownership as described in managed Consumer. Creating the container, starting Consumer and attaching an HTTP endpoint are host responsibilities separate from reading the document.
Security metadata and actual authentication#
consumer-auth in the example is a security scheme in the AsyncAPI document; it does not authenticate an incoming message. Broker credentials, TLS/mTLS and application payload verification are separate runtime mechanisms. Do not include real credentials in the documented server host; the example uses a public placeholder.
The document describes version/content contracts for one physical queue; RabbitMQ does not use that information to select a binary. Every replica sharing the queue must support all contracts publishers can emit. Versioning and rollouts explains that deployment boundary.
Sources for this page v0.1.0
This guide is based on the reviewed framework commit.