Skip to main content

Module openapi

Module openapi 

Source
Available on crate feature openapi only.
Expand description

Use OpenAPI to describe your HTTP API. Documentation tools can display this description, and client generators can use it to create code that calls your API. In Topcoat, you describe routes with Operation and use OpenApi to collect those descriptions into a Document.

Enable Topcoat’s openapi feature. Also add schemars, which describes the data in your request and response bodies:

cargo add schemars

§Documenting a route

Add an Operation to a route using with(...). Use request to describe the request body and response_with to describe a response that has a body. Derive JsonSchema on the data types used in these bodies:

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)))
}

Topcoat gets the path, HTTP methods, and path parameters from the router. You provide the other details in the operation. Topcoat does not check that these details match the handler, so keep them up to date when you change what the handler accepts or returns.

For a response with no body, use response. Doc comments on your data types and their fields become descriptions in the generated schema.

§Serving the document

Set the API’s title, version, and other document settings with OpenApi. Pass your router to generate to create a Document. Return the document from a route to serve it as JSON:

use topcoat::{
    Result,
    context::Cx,
    openapi::{Document, OpenApi},
    router::{route, router},
};

#[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)))
}

router(cx) gives you the router handling the current request. In this example, the document route has no operation of its own. It stays out of the document unless it inherits an operation, as described below.

You can also generate the document outside a request. For example, a test or an application command can build the router, call generate, and save the result with to_json_pretty:

use topcoat::{openapi::OpenApi, router::Router};

fn export(router: &Router) -> std::io::Result<()> {
    let document = OpenApi::new("Acme API", "1.0.0").generate(router);
    std::fs::write("openapi.json", document.to_json_pretty())
}

§Inherited settings

Add an Operation to a layer to set documentation defaults for every route under its path. Each route inherits these defaults and is included in the document. Use .hidden(true) to leave a route out:

use topcoat::{
    Result,
    context::Cx,
    openapi::Operation,
    router::{Body, Next, StatusCode, layer, response::Response, route},
};

#[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")
}

A route can add its own operation to override or extend these defaults. For example, a summary on the route overrides the summary from its layer.

Tags, parameters, and responses are combined. The route keeps the entries from its layers. If it defines the same parameter or a response with the same status code, its definition replaces the inherited one.

You can also put an operation on a layout to document the pages it wraps, or on the router builder with .with(...) to document every route. These settings follow the rules of route context.

§Parameters

Topcoat automatically includes path parameters in the document and describes them as strings. Add a Parameter when you want to specify a different type or add a description:

use topcoat::openapi::{Operation, Parameter};

let operation = Operation::new().parameter(
    Parameter::path("user_id")
        .schema::<u64>()
        .description("The ID of the user."),
);

Use Parameter to describe individual query parameters, headers, or cookies too. These are optional by default. Call required(true) to mark one as required.

To document a struct used with query_params, derive JsonSchema and pass the struct’s type to query. Topcoat adds a query parameter for each field. An Option field is optional, and each field’s doc comment becomes its description:

use schemars::JsonSchema;
use topcoat::{openapi::Operation, router::query_params};

#[query_params]
#[derive(JsonSchema)]
struct PostsQuery {
    /// The text to search for.
    q: String,
    /// The page to show, starting at 1.
    page: Option<u32>,
}

let operation = Operation::new().query::<PostsQuery>();

§Request and response bodies

The Content trait describes a body’s format, called its media type, and the shape of its data, called its schema. JSON and form extractors implement Content when their data type implements JsonSchema. String bodies are described as plain text.

To document an optional request body, wrap the body type in Option. For example, .request::<Option<Json<CreateUser>>>() says that the client may leave out the JSON body.

For a custom extractor or response type, implement Content to describe its body:

use topcoat::openapi::{
    Content,
    schemars::{Schema, SchemaGenerator, json_schema},
};

struct Csv(String);

impl Content for Csv {
    const MEDIA_TYPE: &'static str = "text/csv";

    fn schema(_generator: &mut SchemaGenerator) -> Schema {
        json_schema!({ "type": "string" })
    }
}

Schemas follow Serde’s rules for reading data into a type, even when the type is used in a response. If a type uses different rules for reading and writing, the response schema may differ from the data sent to the client. For example, #[serde(skip_serializing)] leaves a field out when writing a response, but that field can still appear in the schema.

§Authentication

Give each authentication method a name and register it as a SecurityScheme. Use that name in security to document which method a route requires. Put this setting on a layer to apply it to every route below the layer. A route can use .no_security() to document that it does not require authentication:

use topcoat::openapi::{OpenApi, Operation, SecurityScheme};

let api = OpenApi::new("Acme API", "1.0.0")
    .security_scheme("token", SecurityScheme::bearer().bearer_format("JWT"));

// On a layer: every route below requires the token.
let protected = Operation::new().security("token");
// On a route below that layer: usable without the token.
let public = Operation::new().no_security();

Call security more than once on an operation to list alternative authentication methods. Clients can use any one of them. When an operation sets its own security requirements, they replace all the requirements inherited from its layers.

These settings describe authentication in the document. Your layers and handlers must still check credentials and enforce access rules.

Modules§

schemars
Schemars

Structs§

Document
An OpenAPI document that describes the documented routes of a router.
OpenApi
The settings of an OpenAPI document, such as the API’s title and version.
Operation
Describes a route in the API document.
Parameter
A single request parameter: a path segment, a query key, a header, or a cookie.
SecurityScheme
A way for clients to authenticate, such as a bearer token or an API key.

Traits§

Content
A request or response body that an API document can describe.