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を業務で書いてみたい仲間を募集しているので、興味があればカジュアル面談でお話しましょう。
この記事が気に入ったらサポートをしてみませんか?