#spec #actix-web #framework #open-api #generate #axum #oasgen

macro oasgen-macro

Dependency of oasgen. Generates OpenAPI 3.0 spec based on Rust code. Works with actix-web, but architected to easily extend to other frameworks (or no framework).

36 releases (19 breaking)

0.20.1 Feb 15, 2024
0.19.0 Feb 3, 2024
0.18.4 Dec 27, 2023
0.14.0 Oct 31, 2023
0.1.8 Dec 27, 2022

#1543 in Procedural macros

Download history 30/week @ 2023-12-04 9/week @ 2023-12-18 93/week @ 2023-12-25 10/week @ 2024-01-29 184/week @ 2024-02-12 168/week @ 2024-02-19 57/week @ 2024-02-26 23/week @ 2024-03-04 44/week @ 2024-03-11 12/week @ 2024-03-18

140 downloads per month
Used in 3 crates (via oasgen)

MIT license

36KB
842 lines

GitHub Contributors Stars Build Status Downloads Crates.io

oasgen - OpenAPI Spec Generator

oasgen is a library to generate OpenAPI 3.0 specs from Rust server code (or any async functions). It supports:

  • actix - actix-web
  • axum - axum
  • No framework - if you just want to register Rust functions to generate an OpenAPI spec file.

Contributions to support other web frameworks are welcome!

Example

// Actix-web example
use actix_web::web::Json;
use actix_web::{App, HttpServer};
use oasgen::{oasgen, OaSchema, Server};
use serde::{Deserialize, Serialize};

#[derive(OaSchema, Deserialize)]
pub struct SendCode {
    pub mobile: String,
}

#[derive(Serialize, OaSchema, Debug)]
pub struct SendCodeResponse {
    pub found_account: bool,
}

#[oasgen]
async fn send_code(_body: Json<SendCode>) -> Json<SendCodeResponse> {
    Json(SendCodeResponse {
        found_account: false,
    })
}

#[tokio::main]
async fn main() {
    let server = Server::actix().post("/send-code", send_code).freeze();

    HttpServer::new(move || App::new().service(server.clone().into_service()))
        .bind(("127.0.0.1", 5000))
        .unwrap()
        .run()
        .await
        .unwrap()
}
// axum example
use oasgen::{OaSchema, Server, oasgen};
use axum::{Json, routing};
use serde::{Deserialize, Serialize};

#[derive(OaSchema, Deserialize)]
pub struct SendCode {
    pub mobile: String,
}

#[derive(Serialize, OaSchema, Debug)]
pub struct SendCodeResponse {
    pub found_account: bool,
}

#[oasgen]
async fn send_code(_body: Json<SendCode>) -> Json<SendCodeResponse> {
    Json(SendCodeResponse { found_account: false })
}

#[tokio::main]
async fn main() {
    let server = Server::axum()
        .post("/send-code", send_code)
        .freeze();

    let router = axum::Router::new()
        .route("/healthcheck", routing::get(|| async { "OK" }))
        .merge(server.into_router());

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

To compile the axum example, use the following dependencies:

[dependencies]
axum = "0.6"
oasgen = { version = "0.19.0", features = ["axum"] }
serde = { version = "1.0.196", features = ["derive"] }
tokio = { version = "1.36.0", features = ["full"] }

Installation

[dependencies]
# At minimum, you probably want a server feature installed (axum, actix) to support that framework
oasgen = { version = "..", features = []}

There are several features for activating other libraries:

  • actix - actix-web
  • axum - axum
  • swagger-ui - swagger ui
  • uuid - uuid
  • chrono - chrono
  • time - time
  • sqlx - sqlx

Customizing the generated spec

You can customize the generated spec in many ways.

Direct access to the OpenAPI struct

You have direct access to the OpenAPI struct, so you can customize it however you want.

let mut server = Server::new();
server.openapi.info.title = "My API".to_string();
server.openapi.components.schemas.insert("MySchema".to_string(), Schema::new_object());
server
    .get("/my-route", my_handler)
    .freeze();

Note that you must make any changes before calling .freeze() (which moves the OpenAPI struct into an Arc to be shared between threads).

Customizing a Schema

You can hand-write an implementation of OaSchema instead of using derive to customize any Schema. If you do this, call register_schema after the struct definition to add it to the spec.

use oasgen::{OaSchema, Schema, register_schema};

pub struct User {
    pub id: i32,
    pub name: String,
}

impl OaSchema for User {
    fn schema() -> Schema {
        let mut schema = Schema::new_object();
        schema.properties_mut().insert("id", Schema::new_integer());
        schema.properties_mut().insert("name", Schema::new_string());
        schema
    }
}
register_schema!("User", &|| User::schema());

Technically speaking, you don't need to implement OaSchema at all. You can pass any arbitrary closure that returns a Schema to the register_schema macro.

You can also customize an operation:

async fn my_server_handler() {
    // ...
}

// You must use the fully qualified path to the function.
// You can simplify this slightly by passing in `concat!(module_path!(), "::my_server_handler")`
register_operation!("my_server_crate::path::to::my_server_handler", &|| {
    let mut operation = Operation::default();
    operation.summary = Some("My summary".to_string());
    // ...
    operation
});

Attributes

oasgen defines its own attributes, and also respects serde attributes. It also uses docstrings as descriptions. You can see all attributes in macro/src/attr.rs. Look at those structs for relevant documentation, and see the examples below.


#[derive(OaSchema)]
pub struct User {
    pub id: i32,
    pub name: String,
    // Because oasgen respects serde attributes, this will not appear in the spec.
    #[serde(skip)]
    pub password_hash: String,
    // This will be in the response (because there's no serde(skip), but it will not show up in the OpenAPI spec.
    #[oasgen(skip)]
    pub internal_id: i32,
}

#[oasgen(
tags("auth", "users"),
summary = "This is a short summary"),
deprecated = true,
operation_id = "my_operation_id",
description = "This is a long description and will override the docstring of the function",
)]
async fn my_server_handler() {
    // ...
}

Write the spec to a file

You have direct access to the OpenAPI struct. You can use serde to write it to a file, stdout, and more.

We provide a helper function write_and_exit_if_env_var_set that integrates well with a basic build process:

let server = Server::new()
    // your routes
    .write_and_exit_if_env_var_set("./openapi.yaml")
    // .freeze() here, if you're mounting to a server.

If OASGEN_WRITE_SPEC=1, it will write the spec to the path, then exit.

In your build process, build the executable, run it once with the env var set to output the spec, then run it again without the env var to start the server normally.

Route that displays the spec

[!NOTE]
Requires the swagger-ui feature

There are built-in functions to create routes that display the raw spec, or display a Swagger UI page for the spec.

let mut server = oasgen::Server::axum()
    .post("/auth/register_password", auth::register_password) // example route
    .route_yaml_spec("/openapi.yaml") // the spec will be available at /openapi.yaml
    .route_json_spec("/openapi.json") // the spec will be available at /openapi.json
    .swagger_ui("/openapi/"); // the swagger UI will be available at /openapi/.
                              // NOTE: The trailing slash is required, as is calling either `route_yaml_spec()` or `route_json_spec()` before `swagger_ui()`.

If you need to customize these routes, you have directly use a clone of the OpenAPI struct. It's in an Arc, so it's cheap to clone.

let mut server = oasgen::Server::axum()
    .post("/auth/register_password", auth::register_password) // example route
    .freeze();
let spec = server.openapi.clone();
let router = axum::Router::new()
    .merge(server.into_router())
    .route("/alt/route/to/openapi.yaml", get(|| {
        let spec = spec.clone();
        async { 
            serde_yaml::to_string(spec).unwrap()
        }
    }))
;

Dependencies

~2.6–3.5MB
~66K SLoC