- OpenAPI
- Getting started
- Inherited settings
- Parameters, bodies and authentication
- Listing routes
OpenAPI
Getting started
Enable the new openapi feature and add schemars, which describes the data in your request and response bodies:
cargo add topcoat --features openapi
cargo add schemarsAttach an Operation to a route with with(...). Topcoat takes the path, HTTP methods and path parameters from the router, and you add the rest:
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use topcoat::{
Result,
context::Cx,
openapi::Operation,
router::{StatusCode, content::Json, route},
};
#[derive(Deserialize, JsonSchema)]
struct CreateUser {
/// The name to show for the user.
name: String,
}
#[derive(Serialize, JsonSchema)]
struct User {
id: u64,
name: String,
}
#[route(POST "/api/users", with(
Operation::new()
.summary("Create a user")
.request::<Json<CreateUser>>()
.response_with::<Json<User>>(StatusCode::CREATED, "The new user.")
.response(StatusCode::CONFLICT, "The name is taken.")
))]
async fn create_user(cx: &Cx, Json(input): Json<CreateUser>) -> Result<(StatusCode, Json<User>)> {
let user = User { id: 1, name: input.name };
Ok((StatusCode::CREATED, Json(user)))
}Doc comments on your types and fields become descriptions in the generated schema.
Use OpenApi to set the title, version and other document settings, then pass it your router. Return the Document from a route to serve it as JSON:
use topcoat::openapi::{Document, OpenApi};
#[route(GET "/openapi.json")]
async fn spec(cx: &Cx) -> Result<Document> {
Ok(OpenApi::new("Acme API", "1.0.0")
.description("Everything you need for tea.")
.server("https://api.example.com")
.generate(&router(cx)))
}You can also generate the document outside a request, for example in a test or a command that writes openapi.json with Document::to_json_pretty.
Inherited settings
An Operation on a layer sets defaults for every route under its path. Each of those routes is included in the document and inherits the layer's tags, responses and other settings. A route can override or extend them with its own operation, or leave the document with .hidden(true):
#[layer("/api", with(
Operation::new()
.tag("api")
.response(StatusCode::UNAUTHORIZED, "The token is missing or wrong.")
))]
async fn api(cx: &Cx, body: Body, next: Next<'_>) -> Result<Response> {
next.run(cx, body).await
}
// Documented through the layer, with its tag and 401 response.
#[route(GET "/api/health")]
async fn health(cx: &Cx) -> Result<&'static str> {
Ok("ok")
}
// Left out of the document.
#[route(GET "/api/debug", with(Operation::new().hidden(true)))]
async fn debug(cx: &Cx) -> Result<&'static str> {
Ok("debug")
}Operations follow the same rules as route context, so you can also put one on a layout or on the router builder.
Parameters, bodies and authentication
Parameterdescribes path, query, header and cookie parameters. Path parameters are included automatically as strings; add aParameterto give one a type or a description.Operation::query::<T>()documents every field of a#[query_params]struct that derivesJsonSchema.- JSON and form extractors describe their bodies when their data type implements
JsonSchema. Implement theContenttrait to describe a custom body type. SecuritySchemeregisters an authentication method on the document, andOperation::securitymarks the routes that require it. Put it on a layer to protect everything below, and use.no_security()on public routes.
Topcoat does not check that an operation matches its handler, and security settings only describe authentication. Your layers and handlers still enforce access rules.
Read the OpenAPI guide for the details, or run the example:
cargo topcoat dev -p openapiListing routes
The router can now list its routes. Router::routes returns each registered route and page with its ID, served path, HTTP methods and the context values from its layers. Router::context returns the values set on the router itself. The OpenAPI generator is built on these, and you can use them for your own tooling, such as a sitemap or a route listing in development.
for route in router.routes() {
println!("{}", route.path());
}