Crate okapi_operation

source ·
Expand description

okapi-operation

Crate which allow to generate OpenAPI’s operation definitions (using types from okapi crate) with procedural macro openapi.

Example (using axum, but without axum_integration feature)

use axum::{
    extract::Query,
    http::Method,
    routing::{get, post},
    Json, Router,
};
use okapi_operation::*;
use serde::Deserialize;

#[derive(Deserialize, JsonSchema)]
struct Request {
    /// Echo data
    data: String,
}

#[openapi(
    summary = "Echo using GET request",
    operation_id = "echo_get",
    tags = "echo",
    parameters(
        query(name = "echo-data", required = true, schema = "std::string::String",),
        header(name = "x-request-id", schema = "std::string::String",)
    )
)]
async fn echo_get(query: Query<Request>) -> Json<String> {
    Json(query.0.data)
}

#[openapi(
    summary = "Echo using POST request",
    operation_id = "echo_post",
    tags = "echo"
)]
async fn echo_post(
    #[request_body(description = "Echo data", required = true)] body: Json<Request>,
) -> Json<String> {
    Json(body.0.data)
}

async fn openapi_spec() -> Json<OpenApi> {
    let generate_spec = || {
        OpenApiBuilder::new("Echo API", "1.0.0")
            .add_operation("/echo/get", Method::GET, echo_get__openapi)?
            .add_operation("/echo/post", Method::POST, echo_post__openapi)?
            .generate_spec()
    };
    generate_spec().map(Json).expect("Should not fail")
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/echo/get", get(echo_get))
        .route("/echo/post", post(echo_post))
        .route("/openapi", get(openapi_spec));

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

openapi macro

This macro generate function with name <original_name>__openapi of type fn(&mut Components) -> Result<Operation, anyhow::Error> (OperationGenerator), which generate okapi::openapi3::Operation, storing type definitions in provided Components.

If any attribute is missing, it is set to None/false.

Since most attributes taken from OpenAPI specification directly, refer to OpenAPI website for additional information.

Minimal example

Macro doesn’t have any mandatory attributes.

#[openapi]
async fn handler() {}

Operation attributes

All attributes is translated into same fields of okapi::openapi3::Operation.

Tags is provided as single string, which later is separated by comma.

#[openapi(
    summary = "Simple handler",
    description = "Simple handler, demonstrating how to use operation attributes",
    operation_id = "simple",
    tags = "examples,handlers",
    deprecated = false
)]
async fn handler() {}

External documentation

External documentation can be set for operation. It is translated to okapi::openapi3::ExternalDocs.

#[openapi(
    external_docs(
        url = "https://example.com",
        description = "Example Domain"
    )
)]
async fn handler() {}

Request parameters

Request parameters can be:

  • HTTP header (location: header);
  • query parameter (?param=value) (location: query);
  • part of the path (/api/user/:id, where :id is parameter) (location: path);
  • reference to one of the above.

Parameters is defined in [openapi] macro. Inferring header from fucntion signature is not supported currently.

This definition translated to okapi::openapi3::Parameter with okapi::openapi3::ParameterValue::Schema.

header have following attributes:

  • name (string, mandatory);
  • description (string, optional);
  • required (bool, optional);
  • deprecated (bool, optional);
  • style (string, optional) - how parameter is serialized (see OpenAPI docs);
  • schema (path, mandatory) - path to type of parameter.
#[openapi(
    parameters(
        header(
            name = "x-custom-header",
            description = "Custom header description",
            required = true,
            deprecated = false,
            style = "simple",
            schema = "std::string::String",
        )
    )
)]
async fn handler() {}
Query

query have following attributes:

  • name (string, mandatory);
  • description (string, optional);
  • required (bool, optional);
  • deprecated (bool, optional);
  • style (string, optional) - how parameter is serialized (https://swagger.io/docs/specification/serialization/);
  • explode (bool, optional) - specifies whether arrays and objects should generate separate parameters for each array item or object property;
  • allow_empty_value (bool, optional) - allow empty value for this parameter;
  • allow_reserved (bool, optional) - allow reserved characters :/?#[]@!$&'()*+,;= in parameter;
  • schema (path, mandatory) - path to type of parameter.
#[openapi(
    parameters(
        query(
            name = "page",
            description = "Which page to return",
            required = true,
            deprecated = false,
            style = "simple",
            explode = true,
            allow_empty_value = false,
            allow_reserved = false,
            schema = "std::string::String",
        )
    )
)]
async fn handler() {}
Path

path have following attributes:

  • name (string, mandatory);
  • description (string, optional);
  • deprecated (bool, optional);
  • style (string, optional) - how parameter is serialized (https://swagger.io/docs/specification/serialization/);
  • schema (path, mandatory) - path to type of parameter.

Unlike header and query parameters, all path parameters is mandatory.

#[openapi(
    parameters(
        path(
            name = "user_id",
            description = "ID of user",
            deprecated = false,
            style = "simple",
            schema = "std::string::String",
        )
    )
)]
async fn handler() {}

cookie have following attributes:

  • name (string, mandatory);
  • description (string, optional);
  • required (bool, optional);
  • deprecated (bool, optional);
  • explode (bool, optional) - specifies whether arrays and objects should generate separate parameters for each array item or object property;
  • allow_empty_value (bool, optional) - allow empty value for this parameter;
  • schema (path, mandatory) - path to type of parameter.
#[openapi(
    parameters(
        cookie(
            name = "session_id",
            description = "Session ID",
            required = false,
            deprecated = false,
            explode = true,
            allow_empty_value = false,
            schema = "std::string::String",
        )
    )
)]
async fn handler() {}
Reference
#[openapi(
    parameters(
        reference = "#/components/parameters/ReusableHeader"
    )
)]
async fn handler() {}

Multiple parameters

Specifying multiple parameters is supported:

#[openapi(
    parameters(
        header(
            name = "x-request-id",
            description = "ID of request for logging",
            required = true,
            deprecated = false,
            style = "simple",
            schema = "std::string::String",
        ),
        header(
            name = "traceparent",
            description = "ID of parent span",
            required = true,
            deprecated = false,
            style = "simple",
            schema = "std::string::String",
        ),
        path(
            name = "user_id",
            description = "ID of user",
            deprecated = false,
            style = "simple",
            schema = "std::string::String",
        ),
        reference = "#/components/parameters/ReusableHeader"
    ),
)]
async fn handler() {}

Request body

Request body is associated with one of function arguments and by default it’s schema is inferred from argument type.

Request body definition have following attributes:

  • description (string, optional);
  • required (bool, optional);
  • content (path, optional) - path to type, which schema should be used. If not speified, argument’s type is used.
#[derive(JsonSchema)]
struct Request {
    user_id: String
}


#[openapi]
async fn handler(
    #[request_body(
        description = "JSON with user ID",
        required = true,
    )] body: Json<Request>
) {}

#[openapi]
async fn handler_with_request_body_override(
    #[request_body(
        description = "JSON with user ID",
        required = true,
        content = "Json<std::string::String>",
    )] body: Json<Request>
) {}

Responses

Responses can be:

  • inferred from return type;
  • specified in openapi macro.
From return type

Return type should implement ToResponses trait.

#[derive(JsonSchema)]
struct Response {
    data: String
}

#[openapi]
async fn handler() -> Json<Response> {
}
Ignore return type

If return type doesn’t implement ToResponses, it can be ignored with special attribute ignore_return_type:

#[openapi(
    responses(
        ignore_return_type = true,
    )
)]
async fn handler() -> String {
}
Manual definition

Manual definition is helpful when you type for some reason doesn’t implement ToResponses or if you need to specify some responses, which can occur outside handler (in middleware, for example).

Single response

Single response define response for a single HTTP status (or pattern). Schema of this response should implement ToMediaTypes.

Single response have following attributes:

  • status (string, mandatory) - HTTP status (or pattern like 2XX, 3XX). To define defautl fallback type, use special default value;
  • description (string, optional);
  • content (path, mandatory) - path to type, which provide schemas for this response;
  • headers (list, optional) - list of headers (definition is the same as in request parameters). References to header is also allowed.
#[derive(JsonSchema)]
struct Response {
    data: String
}

#[openapi(
    responses(
        response(
            status = "200",
            description = "Success",
            content = "Json<Response>",
            headers(
                header(
                    name = "x-custom-message", 
                    description = "Description",
                    required = true,
                    deprecated = false,
                    style = "simple",
                    schema = "std::string::String",
                ),
                reference = "#/components/headers/ReusableHeader"
            ),
        ),
    )
)]
async fn handler() {
}
From type

Responses can be generated from type, which implement ToResponses:

#[derive(JsonSchema)]
struct Response {
    data: String
}

#[openapi(
    responses(
        from_type = "Json<String>",
    )
)]
async fn handler() {
}

Json<String> generates single 200 response with JSON with single string.

Reference

Reference to response have following attributes:

  • status (string, mandatory) - HTTP status (or pattern like 2XX, 3XX). To define defautl fallback type, use special default value;
  • reference (string, mandatory).
#[openapi(
    responses(
        reference(
            status = "200",
            reference = "#/components/responses/Reference"
        )
    )
)]
async fn handler() {
}
Multiple responses

If mutliple manual responses is specified (or specified both return type and manual responses), they are all merged using okapi::merge::merge_responses. If multiple responses specified for same HTTP status, first occurence is used. Responses merged in following order:

  • from return type;
  • manual single responses;
  • references;
  • from types.
#[derive(JsonSchema)]
struct Response {
    data: String
}

#[openapi(
    responses(
        response(
            status = "500",
            description = "Internal server error",
            content = "Json<String>",
        ),
        reference(
            status = "401",
            reference = "#/components/responses/AuthError"
        ),
        reference(
            status = "403",
            reference = "#/components/responses/AuthError"
        )
    )
)]
async fn handler() -> Json<Response> {
}

Security scheme

Security scheme have following attributes:

  • name (string, mandatory) - name of used security scheme;
  • scopes (string, optional) - comma separated list of scopes. Have meaning only for OAuth2 and OpenID Connect.

If multiple schemes specified, they are combined as OR. AND is not currently supported.

#[openapi(
    security(
        security_scheme(
            name = "BasicAuth",
        ),
        security_scheme(
            name = "OAuth2",
            scopes = "scope1,scope2",
        ),
    ),
)]
async fn handler() {}

Building OpenAPI specification

For convenience this crate provide builder-like OpenApiBuilder type for creating OpenAPI specification:

#[derive(JsonSchema)]
struct Request {
    user_id: String
}


#[openapi]
async fn handler1(
    #[request_body(
        description = "JSON with user ID",
        required = true,
    )] body: Json<Request>
) {
}

#[openapi]
async fn handler2() -> Json<String> {
}

fn generate_openapi_specification() -> Result<OpenApi, anyhow::Error> {
    OpenApiBuilder::new("Demo", "1.0.0")
        .add_operation("/handle/1", Method::POST, handler1__openapi)?
        .add_operation("/handle/2", Method::GET, handler2__openapi)?
        .generate_spec()
}

assert!(generate_openapi_specification().is_ok());

Features

  • macro: enables re-import of openapi macro (enabled by default);
  • axum-integration: enables integration with axum crate (implement traits for certain axum types). See crate::axum_integration for details.

TODO

  • support examples on MediaType or Parameter (examples supported on types via JsonSchema macro)
  • support inferring schemas of parameters from function definitions
  • support for renaming or changing paths to okapi/schemars/okapi-operations in macro
  • more examples

Re-exports

  • pub use okapi;
  • pub use okapi::openapi3::OpenApi;
  • pub use okapi::schemars;
  • pub use okapi::schemars::JsonSchema;
  • pub use okapi::schemars::JsonSchema;
  • pub use anyhow;

Modules

Macros

Structs

Traits

Type Aliases

Attribute Macros