MIXI DEVELOPERS NOTE
🦀

AxumでWebSocketを使うときにOpenAPI Generatorのカスタムテンプレートを使用してハンドラーの生成をする知見

2024/06/18に公開

最近OpenAPI Generatorにrust-axumが追加されているのを発見し、以前開発していたWebSocketサーバーに適用出来ないか試してみました。

library

axumとOpenAPI Generatorのバージョンは以下の指定をしています。

[dependencies.axum]
version = '0.7.5'
features = [
    'ws',
]

OpenAPI Generator: v7.5.0

Generateに使うYaml

生成用のYamlは以下でPath Parameterにchannel_idを入れています。
また、特殊な記述としてx-websocketを入れています。これは後で説明するカスタムテンプレートを使う上で必要になる記述です。

openapi: 3.1.0
info:
  title: WebSocket
  version: 0.0.1
servers:
- url: http://localhost:8088
paths:
  /websocket/{channel_id}:
    get:
      description: WebSocketに接続する
      operationId: connectWebSocket
      x-websocket: true
      servers:
      - url: ws://localhost:8088
      parameters:
      - in: path
        name: channel_id
        required: true
        schema:
          type: string
      - in: header
        name: Connection
        required: true
        schema:
          type: string
      - in: header
        name: Upgrade
        required: true
        schema:
          type: string
      - in: header
        name: Sec-WebSocket-Key
        required: true
        schema:
          type: string
      - in: header
        name: Sec-WebSocket-Version
        required: true
        schema:
          type: string
      responses:
        "101":
          headers:
            Connection: 
              required: true
              schema:
                type: string
            Upgrade:
              required: true
              schema:
                type: string
            Sec-WebSocket-Accept:
              required: true
              schema:
                type: string
          description: 101 response
        "default":
          content:
            text/plain:
              schema:
                type: string
          description: Error response
      tags:
      - websocket

カスタムテンプレートの作成

デフォルトのテンプレートだと生成されるコードが不十分なのでカスタムテンプレートを作ります。
x-websocketがyamlにある場合ws::WebSocketUpgradeというExtractorがhandlerの引数に追加されるようになります。

lib.mustache

--- lib.mustache.org	2024-06-18 21:44:16
+++ lib.mustache	2024-06-18 20:40:17
@@ -88,6 +88,9 @@
                 {{#x-consumes-multipart-related}}
                     body: axum::body::Body,
                 {{/x-consumes-multipart-related}}
+                {{#x-websocket}}
+                web_socket: ws::WebSocketUpgrade
+                {{/x-websocket}}
                 ) -> Result<{{{operationId}}}Response, String>;
             {{/vendorExtensions}}

server-operation.mustache

--- server-operation.mustache.org	2024-06-18 21:41:21
+++ server-operation.mustache	2024-06-18 20:40:17
@@ -15,6 +15,9 @@
 {{/queryParams.size}}
  State(api_impl): State<I>,
 {{#vendorExtensions}}
+{{#x-websocket}}
+ web_socket: ws::WebSocketUpgrade,
+{{/x-websocket}}
 {{^x-consumes-multipart-related}}
 {{^x-consumes-multipart}}
   {{#bodyParam}}
@@ -164,6 +167,9 @@
         query_params,
       {{/queryParams.size}}
       {{#vendorExtensions}}
+        {{#x-websocket}}
+        web_socket,
+        {{/x-websocket}}
         {{^x-consumes-multipart-related}}
         {{^x-consumes-multipart}}
           {{#bodyParams}}

コード生成と統合

Apiというトレイトが生成されるのでそれを実装していく形式となります。
あとはtodo()の部分にWebSocketのメッセージのstreamを扱う処理を書くことになります。

struct ServerImpl {
   // database: sea_orm::DbConn,
}

#[allow(unused_variables)]
#[async_trait]
impl websocket::Api for ServerImpl {
    #[doc = r" ConnectWebSocket - GET /websocket/{channel_id}"]
    #[must_use]
    #[allow(clippy::type_complexity, clippy::type_repetition_in_bounds)]
    fn connect_web_socket<'life0, 'async_trait>(
        &'life0 self,
        method: Method,
        host: Host,
        cookies: CookieJar,
        header_params: models::ConnectWebSocketHeaderParams,
        path_params: models::ConnectWebSocketPathParams,
        web_socket: ws::WebSocketUpgrade,
    ) -> ::core::pin::Pin<
        Box<
            dyn ::core::future::Future<Output = Result<ConnectWebSocketResponse, String>>
                + ::core::marker::Send
                + 'async_trait,
        >,
    >
    where
        'life0: 'async_trait,
        Self: 'async_trait,
    {
        todo!()
    }
}

まとめ

OpenAPI Generatorでrust-axumを使ってコードを生成してみましたがWebSocketなど追加でExtractorが必要になってくる場合はテンプレートを書き換える必要が頻繁に出てくるといった印象でした。
また、最終的に生成されるコードとしてRouterまで生成されるので.layer等を好きなように挿入出来ないです。なので生成されたものすべてを使うかはプロジェクトによって精査は必要かと思います。
シンプルなサーバーを生成する上ではとても便利だと思うのでより良い使い方を見つけていけたらと思いました。

GitHubで編集を提案
MIXI DEVELOPERS NOTE
MIXI DEVELOPERS NOTE

Discussion