💭

Rustでutoipaを使ってOpenAPIドキュメントを公開する

2023/01/17に公開

今回はutoipaを使って、Rustで実装したAPIを確認できるようにします。

TL;DR

  1. utoipaをcloneする。
  2. examplesディレクトリにある好きな環境で、cargo run 実行する。
  3. http://localhost:8080/swagger-ui へアクセスする。
  4. Swagger UIでAPIが確認できる。

始めに

Rustで実装したバックエンドのAPIドキュメントを作成するために、
Rustだけでどうにかならないかなと検索したら、utoipaというライブラリで解決できそうだったので、組み込んでSwagger UIで確認してみました。
がんばればOpenAPI Generatorが生成したコードを使えるようです(参考記事

utoipaとは

https://github.com/juhaku/utoipa

Want to have your API documented with OpenAPI? But you dont want to see the
trouble with manual yaml or json tweaking? Would like it to be so easy that it
would almost be like utopic? Don't worry utoipa is just there to fill this
gap. It aims to do if not all then the most of heavy lifting for you enabling
you to focus writing the actual API logic instead of documentation. It aims to
be minimal, simple and fast. It uses simple proc macros which you can use to
annotate your code to have items documented.

utoipaのREADMEより引用

要約するとRustだけで、よい感じにドキュメントを作れるよという感じです。
今回はactix-Webを使いますが、ほかのフレームワーク(axum、warp、tide、rocket)にも対応しているようです。

ゴール

今回はactix-Webでバックエンドを実装し、utoipaを使って以下のようにブラウザでAPIを確認できるようにします。
スクリーンショット 2022-12-07 17.58.12.png

環境

今回使用するコードはこちらにあります。

利用ライブラリ

ディレクトリ

レイヤードアーキテクチャでディレクトリは構成されています。ルーティングなどはapiディレクトリに実装されています(ディレクトリ構造はもっと工夫できると思います)。
ディレクトリ構造は以下のとおりです。

./src
├── api
│   ├── composition.rs // 依存性注入
│   ├── execute.rs // usecase層を使た実装
│   ├── request.rs // リクエスト用のモデル
│   ├── response.rs // レスポインス用のモデル
│   ├── route  // executeとcompositionを使ったAPI(パス)の実装
│   └── route.rs // OpanAPIの設定とactix-webの設定
├── api.rs
├── domaim  // ドメインの実装
├── domain.rs
├── infra  // DBモックの実装
├── infra.rs
├── main.rs  // サーバーの実装
├── usecase  // 各ユースケースの実装
└── usecase.rs

使い方

utoipaを使うためにはToSchema、IntoParams、Path、OpanAPIというDeriveをStructか関数に追加する必要があります(これだけでよい!)。

ToSchema

ToSchemaはrequestやresponseのStructに対して追加します。(ドキュメント

api/request.rs
use utoipa::ToSchema;

#[derive(Deserialize, ToSchema)]
pub struct RegisterUserRequest {
    name: String,
    email: String,
}

IntoParams

IntoParamsはエンドポイントのパラメータをStructとする場合に追加します。(ドキュメント
example/{name} のパラメータをNameというStructで定義した場合は以下のとおりです。

api/request.rs
use utoipa::IntoParams;

#[derive(Deserialize, IntoParams)]
pub struct Name {
    /// User name
    name: String,
}

ドキュメントコメントにパラメータの説明が書けます。

Path

エンドポイントとして指定した関数に追加します。(ドキュメント

api/route/register_user.rs
use actix_web::error::ErrorInternalServerError;
use actix_web::error::ErrorNotFound;
use actix_web::{get, post, web, Responder, Result};

#[utoipa::path(
    post,
    request_body = RegisterUserRequest,
    responses(
        (status = 200, description = "Register User", body = RegisterUserRequest),
        (status = 500, description = "Internal error")
    ),
)]
#[post("/register")]
pub async fn register_user(req: web::Json<RegisterUserRequest>) -> Result<impl Responder> {
    let name = req.0.name();
    let email = req.0.email();
    let Ok(user) = Composition::register_user().run(name, email).await
        else {return Err(ErrorInternalServerError("InternalError"))};
    Ok(web::Json(RegisterUserResponse::from(user)))
}

#[utoipa::path(
    get,
    context_path = "/user",
    params(Name),
    responses(
        (status = 200, description = "Search User", body = SearchedUserResponse),
        (status = 404, description = "Not found")
    ),
)]
#[get("/{name}")]
pub async fn search_user(req: web::Path<Name>) -> Result<impl Responder> {
    let name = req.name();
    let Ok(user) = Composition::search_user().run(name).await
        else { return Err(ErrorNotFound("NotFound"))};
    Ok(web::Json(SearchedUserResponse::from(user)))
}

Pathには以下を設定できます。

  • operation
    • 必ず最初に指定するパラメータ。例:get, post, put, delete, head, options, connect, path, trace
  • path
    • 記述しないとactix-webで設定したエンドポイントになるようです
  • operation_id
    • デフォルトは関数名になるようです
  • context_path
    • スコープが指定してある場合、ドキュメントでエンドポイントの前に表示できます
  • tag
  • request_body
    • 定義したstructを指定できます
  • response
    • 複数のレスポンスが指定できます
  • params
    • IntoParamsを追加したStructを指定できます
  • security

OpenAPI

OpenApiはドキュメント化したいものをまとめます。(ドキュメント

api/route.rs
#[derive(OpenApi)]
#[openapi(
    paths(
        route::check_health::check_health,
        route::register_user::register_user,
        route::post_tweet::post_tweet,
        route::user::search_user::search_user,
        route::tweets::get_all_tweets::get_all_tweets,
    ),
    components(schemas(
        RegisterUserRequest,
        PostTweetRequest,
        RegisterUserResponse,
        PostTweetResponse,
        SearchedUserResponse,
        GetAllTweetResponse
    ))
)]
struct ApiDoc;

openapiには以下が指定できます。

  • paths
    • #[utoipa::path] を追加した関数を追加します
  • components(schema, response)
    • schemas
      • ToSchemaDeriveを追加したStructを追加します
    • response
      • ToResponseTraitを実装したStructを追加します
  • modifiers
  • security
  • tags
  • external_docs

actix-webへの組み込み

main.rs に通常のservice登録と別にswagger-uiを見るためのエンドポイントを追加します。

api/route.rs
use actix_web::{App, HttpServer};

pub fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(check_health::check_health)
        .service(register_user::register_user)
        .service(post_tweet::post_tweet)
        .configure(user::config)
        .configure(tweets::config)
        .service(
            SwaggerUi::new("/swagger-ui/{_:.*}")
                .url("/api-doc/opanapi.json", ApiDoc::openapi()),
        );
}
main.rs
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().configure(config))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

cargo run でサーバを起動することで、http://localhost:8080/swagger-ui/ へアクセスすると上記のようにドキュメントが見られます。

終わりに

特段難しいことはせずにRustのコードだけでAPIドキュメントが公開できます。 ディレクトリ構造を工夫することでOpenAPIっぽくなると思います。
ここでは紹介していない機能もあるので、もっと活用できると思います。

GitHubで編集を提案

Discussion