見出し画像

OpenAPIによるRustのコード生成

こんにちは、株式会社POLでエンジニアをしている高橋です。

今回は https://github.com/OpenAPITools/openapi-generator を使ったRustのコード生成について紹介しようと思います。Rustの場合での生成についてはあまり情報がなかったので、誰かの一助になれば幸いです。

Open API Generatorの設定

以下のようなOpenAPIのyamlファイルについて、コード生成を行う場合について紹介します。サンプルの定義なので細かいOpenAPIの定義はしていないですm(_ _)m

メンテナンス性を向上させるために、OpenAPIのドキュメントはパス毎にファイル分割して定義しています。

# tree -I node_modulesで出力したドキュメントプロジェクトの構造
├── docs
│   ├── api.yaml
│   ├── paths
│   │   └── sample
│   │       └── index.yaml
│   └── schemas
│       ├── models
│       │   └── SampleModel.yaml
│       ├── requests
│       └── responses
│           └── sample
│               └── SampleListResponse.yaml
├── openapitools.json
├── package.json
└── yarn.lock
# docs/api.yaml
openapi: "3.0.2"
info:
 title: LabBase Admin API
 version: "1.0"
servers:
 - url: https://example.com/v1
   description: Example API Server
   variables:
     api_version:
       default: "v1"
       enum:
         - "v1"
paths:
 /sample:
   $ref: ./paths/sample/index.yaml
# docs/paths/sample/index.yaml
get:
  description: "サンプル"
  tags: [samples]
  responses:
    "200":
      description: "サンプル一覧取得"
      content:
        application/json:
          schema:
            $ref: "../../schemas/responses/sample/SampleListResponse.yaml"
# docs/schemas/responses/sample/SampleListResponse.yaml
type: object
properties:
  samples:
    type: array
    items:
      $ref: ./../../models/SampleModel.yaml
# docs/schemas/models/SampleModel.yaml
type: object
properties:
  id:
    type: integer
    nullable: false
    format: int64
    example: 12345
  name:
    type: string
    nullable: false
    default: ""
    example: "名称"

openapi-generator-cliコマンドを使って上記のOpenAPIドキュメントを入力に設定し、Rustのコードを生成します。openapi-generator-cliコマンドをグローバルインストールしたくないので、package.jsonに依存性を記載することでnpm or yarnコマンドで実行できるようにしてます。

# docs/package.json
{
  "name": "sample-api-cogegen",
  "version": "0.1.0",
  "license": "MIT",
  "scripts": {
    "generate": "openapi-generator-cli generate -g rust-server -i docs/api.yaml -o ../path/to/output"
  },
  "devDependencies": {
    "@openapitools/openapi-generator-cli": "^2.5.1"
  }
}
# docs/openapitools.json
# generatorは6.0を使っています
{
  "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
  "spaces": 2,
  "generator-cli": {
    "version": "6.0.0"
  }
}

生成されたRustコードの利用

yarn run generateを実行するとdocs/api.yamlに記載されたAPI定義に基づいてコード生成がされます。hyperベースでのコードがサーバー側、クライアント側どちらも生成されます。(Open API Generatorのversion 6.0.0時点、他のフレームワークも今後対応されると嬉しいですね)

examples配下に利用方法も実装されているので、そのコードをもとに実際に利用するコードを実装します。

  • openapi_client::Apiというtraitの各メソッドを実装する

    • APIのエンドポイントに対応しています

  • openapi_client::Api traitを実装したstructに対して、examplesに実装されているようにopenapi_client::server::MakeService, openapi_client::server::context::MakeAddContextを適用し、hyperで配信可能なAPIサーバーへと変換する

  • openapi_client::Api traitを実装したstructのフィールドに、実際のデータの取得、永続化を実行できるstructを保持する

上記のような流れでOpenAPIから自動生成されたコードベースで、APIサーバーを開発できます。

// main.rs
use std::env;

use dotenv::dotenv;

use anyhow::Result;
use chrono;
use fern
use log;

// presentationでサーバーの生成処理を隠蔽します
mod presentation;

#[tokio::main]
async fn main() -> Result<()> {
    dotenv().ok();
    setup_logger()?;
    let addr = env::var("SERVER_ADDRESS")?.parse()?;
    log::info!("ServiceServer listening on: {}", addr);

    let database_url = env::var("DATABASE_URL")?.parse()?;
    presentation::create_server(addr, database_url).await?;

    Ok(())
}

fn setup_logger() -> Result<()> {
    fern::Dispatch::new()
        .format(|out, message, record| {
            out.finish(format_args!(
                "{}[{}][{}] {}",
                chrono::Local::now().format("[%Y-%m-%d][%H:%M:%S]"),
                record.target(),
                record.level(),
                message,
            ))
        })
        .level(log::LevelFilter::Info)
        .chain(std::io::stdout())
        .apply()?;
    Ok(())
}
// presentation.rs
use std::marker::PhantomData;
use std::sync::Arc;

use hyper::server::Server as HyperServer;
use swagger::EmptyContext;
use swagger::{ApiError, Has, XSpanIdString};
use anyhow::Result;
use async_trait::async_trait;

use openapi_client::{server::MakeService, Api, SampleGetResponse};
use infrastructure::repository::{repository_factory, Repositories};

pub mod sample;

#[derive(new)]
pub struct Server<C> {
    marker: PhantomData<C>,
    // リクエストに対してレスポンスを返却する処理の具体をView側に委譲しています
    sample_view: Arc<sample::View>,
}

// 
impl<C> Server<C> {
    pub fn new(sample_view: Arc<sample::View>) -> Self {
        Server {
            sample_view,
            marker: PhantomData,
        }
    }
}

// main関数で呼ばれるサーバー生成処理です
pub async fn create_server(addr: String, database_url: String) -> Result<()> {
    // viewを生成します。
    // 今回はDBからの取得を想定しているため、database_urlを引数に生成します
    let sample_view = Arc::new(sample::View::new(&database_url).await?);
    let server = Server::new(Arc::clone(&sample_view));
    let service = MakeService::new(server);
    let service = openapi_client::server::context::MakeAddContext::<_, EmptyContext>::new(service);
    HyperServer::bind(&addr.parse()?).serve(service).await?;

    Ok(())
}

// 各APIのエンドポイントを実装します
#[async_trait]
impl<C> Api<C> for Server<C>
where
    C: Has<XSpanIdString> + Send + Sync,
{
    async fn sample_get(&self, _context: &C) -> Result<SampleGetResponse, ApiError> {
        // structで保持しているviewのメソッドを元に値を取得します
        self.sample_view::get_list().await
    }
}

おわり

以上、Open API GeneratorによるRustコードの利用の仕方の紹介でした。Rustでもなるべくスキーマ駆動開発をしたかったので、OpenAPIが使えそうで満足です。

Rustを業務で書いてみたい仲間を募集しているので、興味があればカジュアル面談でお話しましょう。



この記事が気に入ったらサポートをしてみませんか?