oat++でSwaggerのUI/Specを自動生成する!

はじめに

福岡のyoshihitohです。前回の記事で試した oat++ にSwagger関連の機能が用意されていたので試してみました。

検証環境

  • macOS: 10.13.6
  • Command Line Tools for Xcode: 10.0.0.0.1.1535735448
  • Premake: 5.0 alpha 13
  • CMake: 3.9.0

試してみる

前回のコードをベースにSwaggerの機能を組み込んでいきます。

プロジェクト全体のソースは下記リポジトリに置いてあります。

oatpp-swaggerを組み込む

Swagger関連の機能は別モジュールになっているのでGitHubからクローンしてきます。

$ cd external
$ git clone https://github.com/oatpp/oatpp-swagger

oatpp-swaggerはビルド用のスクリプトが用意されていないようです。前回作ったpremakeの設定ファイルにビルドルールを追加します。

+project "oatpp-swagger"
+    kind "StaticLib"
+    language "C++"
+
+    includedirs {
+        "external",
+    }
+
+    files {
+        "external/oatpp-swagger/**.cpp",
+        "external/oatpp-swagger/**.hpp",
+    }
+
 project "oatpp-book"
     kind "ConsoleApp"
     language "C++

oatpp-bookプロジェクトの設定を変更して、 oatpp-swagger をリンクするようにします。

 project "oatpp-book"
     kind "ConsoleApp"
     language "C++"
@@ -36,5 +49,6 @@ project "oatpp-book"
     }

     links {
-        "oatpp"
+        "oatpp",
+        "oatpp-swagger"
     }

ここまででoatpp-swaggerの組み込み準備は完了です。簡単ですね。

エンドポイントの情報を設定する

BookControllerのエンドポイントの情報を設定します。前回は ENDPOINT マクロでエンドポイントを実装しました。今回は ENDPOINT_INFO でエンドポイントの概要やリクエスト・レスポンスの情報を設定します。

 #include OATPP_CODEGEN_BEGIN(ApiController)

+    ENDPOINT_INFO(allBooks) {
+        info->summary = "すべての本を取得する";
+        info->addResponse<decltype(m_database->allBooks())>(Status::CODE_200, "application/json");
+    }
     ENDPOINT("GET", "api/books", allBooks) {
         return createDtoResponse(Status::CODE_200, m_database->allBooks());
     }

+    ENDPOINT_INFO(getBook) {
+        info->summary = "IDを指定して本を取得する";
+        info->addResponse<BookDto::ObjectWrapper>(Status::CODE_200, "application/json");
+        info->addResponse<String>(Status::CODE_404, "text/plain");
+    }
     ENDPOINT("GET", "api/books/{book_id}", getBook, PATH(Int32, book_id)) {
         const auto book = m_database->getBook(book_id);
         OATPP_ASSERT_HTTP(book, Status::CODE_404, "book not found");
         return createDtoResponse(Status::CODE_200, book);
     }

+    ENDPOINT_INFO(putBook) {
+        info->summary = "IDを指定して本を更新する";
+        info->addConsumes<BookDto::ObjectWrapper>("application/json");
+        info->addResponse<BookDto::ObjectWrapper>(Status::CODE_200, "application/json");
+        info->addResponse<String>(Status::CODE_404, "text/plain");
+    }
     ENDPOINT("PUT", "api/books/{book_id}", putBook,
              PATH(Int32, book_id),
              BODY_DTO(BookDto::ObjectWrapper, book_dto)) {

サマリは日本語情報を使えて、リクエスト・レスポンスの形式は型情報を与えるだけで良いみたいです。楽できてありがたいですね!

Swaggerコンポーネントを作る

前回も参考にした crudSwaggerComponent を参考に実装します。

#pragma once

#include "oatpp/core/macro/component.hpp"

#include "oatpp-swagger/Model.hpp"
#include "oatpp-swagger/Resources.hpp"

class SwaggerComponent
{
public:
    OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::swagger::DocumentInfo>, swagger_document_info)([] {
        oatpp::swagger::DocumentInfo::Builder builder;

        builder
            .setTitle("Book management service")
            .setDescription("Book API Example project with swagger docs")
            .setVersion("1.0")
            .setContactName("yoshihitoh")
            .setContactUrl("https://classmethod.jp")
            .addServer("http://localhost:8002", "server on localhost");

        return builder.build();
    }());

    OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::swagger::Resources>, swagger_resources)([] {
        return oatpp::swagger::Resources::loadResources("./external/oatpp-swagger/res");
    }());
};

Swaggerドキュメントの設定と、Webページのレンダリングで使うリソースのパスを指定します。今回はルートディレクトリから実行する前提の相対パスを指定しています。

SwaggerComponentを有効化する

最後にSwaggerのコンポーネントを組み込んで、コントローラを有効化します。

まず、 AppComponentSwaggerComponent を追加します。

 #include "db/database.hpp"
 #include "db/memory_database.hpp"
+#include "swagger_component.hpp"

 class AppComponent {
 public:
+  SwaggerComponent swagger_component;
+
   OATPP_CREATE_COMPONENT(std::shared_ptr<oatpp::network::ServerConnectionProvider>, serverConnectionProvider)([] {
     return oatpp::network::server::SimpleTCPConnectionProvider::createShared(8002);
   }());

次にmain関数の起動処理で、Swaggerのコンポーネント・コントローラを有効化します。

 #include <iostream>

+#include "oatpp-swagger/Controller.hpp"
+
 #include "controller/book_controller.hpp"
 #include "app_component.hpp"
 #include "logger.hpp"
@@ -15,6 +17,12 @@ static void run()
     auto book_controller = BookController::createShared();
     book_controller->addEndpointsToRouter(router);

+    auto doc_endpoints = oatpp::swagger::Controller::Endpoints::createShared();
+    doc_endpoints->pushBackAll(book_controller->getEndpoints());
+
+    auto swagger_controller = oatpp::swagger::Controller::createShared(doc_endpoints);
+    swagger_controller->addEndpointsToRouter(router);
+
     oatpp::network::server::Server server(components.serverConnectionProvider.getObject(),
                                           components.serverConnectionHandler.getObject());
     OATPP_LOGD("Server", "Running on port %u...", components.serverConnectionProvider.getObject()->getPort());

以上でSwagger機能の準備は完了です。

動作確認

ビルドして起動する

実際に動かしてみましょう。前回と同様の手順でビルドします。

# プロジェクトファイル(Makefile)を更新
$ premake5 gmake2

# ビルド
$ make -j$(sysctl -n hw.ncpu)

私の環境だとこの指定でビルドすると下記のエラーが発生しました。

$ make clean && make -j$(sysctl -n hw.ncpu)
...
==== Building oatpp-book (debug) ====
Creating obj/Debug/oatpp-book
book.cpp
main.cpp
memory_database.cpp
Linking oatpp-book
ld: warning: ignoring file bin/Debug/liboatpp-swagger.a, file was built for archive which is not the architecture being linked (x86_64): bin/Debug/liboatpp-swagger.a
Undefined symbols for architecture x86_64:

oatpp-swaggerを静的ライブラリとしてビルドするときに、binutilsの arranlib でアーカイブしてしまったようです。下記の記事を参考にパスを明示することで解消できました。

$ make clean && AR="/usr/bin/ar" RANLIB="/usr/bin/ranlib" make -j$(sysctl -n hw.ncpu)
...
==== Building oatpp-book (debug) ====
Creating obj/Debug/oatpp-book
book.cpp
main.cpp
memory_database.cpp
Linking oatpp-book
bash-4.4$

前回と同様に起動します。今回はSwaggerのリソースパスを相対パスで指定しているので、ルートディレクトリから起動します。

$ ./build-gmake/bin/Debug/oatpp-book
Server:Running on port 8002...

Swagger-UIを確認する

起動したらSwagger-UIのページ http://localhost:8002/swagger/ui にアクセスしてみます。

ちゃんとエンドポイントの定義が追加されていますね!試しにPUTの詳細を見てみると

パスとリクエストのパラメタについてちゃんと反映されています。

レスポンスの情報も設定したとおりですね。

DTOオブジェクトのスキーマもばっちりです。

Swagger-Specを確認する

http://localhost:8002/api-docs/oas-3.0.0.json にアクセスしてSwagger-Specを出力します。JSON形式で出力できるんですが、書式が少し冗長なのでYAML形式に変換してみます。JSON→YAMLの変換は下記のエントリを参考にRubyのワンライナーを使います。

$ curl -XGET http://localhost:8002/api-docs/oas-3.0.0.json | ruby -ryaml -rjson -e 'puts YAML.dump(JSON.parse(STDIN.read))'
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  1876  100  1876    0     0   267k      0 --:--:-- --:--:-- --:--:--  305k
---
openapi: 3.0.0
info:
  title: Book management service
  description: Book API Example project with swagger docs
  contact:
    name: yoshihitoh
    url: https://classmethod.jp
  version: '1.0'
servers:
- url: http://localhost:8002
  description: server on localhost
paths:
  "/api/books":
    get:
      summary: すべての本を取得する
      operationId: allBooks
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  "$ref": "#/components/schemas/BookDto"
    parameters: []
  "/api/books/{book_id}":
    get:
      summary: IDを指定して本を取得する
      operationId: getBook
      responses:
        '404':
          description: Not Found
          content:
            text/plain:
              schema:
                type: string
        '200':
          description: OK
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/BookDto"
    put:
      summary: IDを指定して本を更新する
      operationId: putBook
      requestBody:
        description: request body
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/BookDto"
      responses:
        '404':
          description: Not Found
          content:
            text/plain:
              schema:
                type: string
        '200':
          description: OK
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/BookDto"
    parameters:
    - name: book_id
      in: path
      required: true
      schema:
        type: integer
        format: int32
components:
  schemas:
    BookDto:
      type: object
      properties:
        id:
          type: integer
          format: int32
        title:
          type: string
        author:
          type: string
        isbn:
          type: string
        publish_at:
          type: integer
          format: int64

こちらも問題なく、設定した通りに出力されていますね!

おわりに

今回は oatpp-swagger を利用して、Swagger-UIとSwagger-Specを自動生成してみました。今までWAFのSwagger統合機能を使ったことがなかったので衝撃的でした。私は知らなかったのですが、Swagger統合をサポートしているWAFや同等のことを実現するツールが沢山あるようです。

Swagger-SpecとAPIの実装を同じコードで管理できると定義と実装の乖離を防げてメンテが楽になりそうですね。機会があれば業務でも活用していきたいと思います。

参考