はじめに
弊社では、API の設計に OpenAPI を活用しよう、という取り組みをしています。元はドキュメント作成を効率化しよう、というものでした。
それとは関係なく、開発作業の効率化を目的として、設計内容からソースコードの一部を自動生成する、という取り組みも進められています。
さて、この 2 つの取り組みを知ると、OpenAPI のファイルからソースコードを自動生成してみよう、ということを思いつくわけですが、社内で実例を見たことががなかったので、試してみます。
サンプルコード
こちらに置きました。
目標
小さな OpenAPI のファイルから、Rust Actix-Web に組み込む、モデルとハンドラー部分のソースコードを生成してみます。
ファイルは → こちら です。backend.openapi.yaml というファイルです。
使用するツール
ここでは openapi-generator を使用します。とくに選定理由はなく、ひとまず目に付いたツールを使ってみた、というところです。
カスタムジェネレータ
openapi-generator はたくさんの開発言語用のソースコードジェネレータを備えており、その中には Rust もありますが、Actix-Web はないようです。あったとしても、独自のカスタマイズは必要になると思われるので、最初から カスタムジェネレータ で進めることにします。
ひな型の作成
openapi-generator のドキュメントに従って、カスタムジェネレータを作成します。ここでは、fab というディレクトリで作業することにします。
mkdir fab
cd fab
npm init -f
npm install @openapitools/openapi-generator-cli -D
npx openapi-generator-cli meta -o out/generators/actix-web -n actix-web
とりあえずビルド
生成されっぱなしで何も変更せず、ひとまずビルドできるか確認してみます。
cd out/generators/actix-web
mvn package
:
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Concurrency config is parallel='methods', perCoreThreadCount=true, threadCount=2, useUnlimitedThreads=false
Running org.openapitools.codegen.ActixWebGeneratorTest
[main] WARN io.swagger.v3.parser.OpenAPIV3Parser - Exception while reading:
io.swagger.v3.parser.exception.ReadContentException: Unable to read location `../../../modules/openapi-generator/src/test/resources/2_0/petstore.yaml` at io.swagger.v3.parser.OpenAPIV3Parser.readContentFromLocation(OpenAPIV3Parser.java:295)
:
なにやらエラーになりました。
../../../modules/openapi-generator/src/test/resources/2_0/petstore.yaml
というファイルがないらしいのですが、たしかにそのパスにファイルはありません。
仕方がないので、openapi-generator の GitHub のリポジトリから 該当パスのファイル をコピーして作成し、もう一度実行します。
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
成功しました。正しい対処方法だったのかはわかりませんが、気にしないことにします :-)
ソースコードの生成
テンプレートが初期状態のままですが、とりあえずソースコードの生成ができるかを試します。fab というディレクトリに戻って実行します。
cd ../../..
npx openapi-generator-cli --custom-generator=out/generators/actix-web/target/actix-web-openapi-generator-1.0.0.jar generate \
-g actix-web \
-i ../backend.openapi.yaml \
-o generated/actix-web
tree generated/actix-web/src/
generated/actix-web/src/
└── org
└── openapitools
├── api
│ └── DefaultApi.sample
└── model
├── Book.sample
├── BookRequest.sample
└── User.sample
中身もファイル名もデフォルトのままですが、生成されました。
テンプレートの編集
mustache
openapi-generator の標準のテンプレートエンジンは、Java で動く mustache です。jmustache というライブラリのようです。
Actix-Web のハンドラー用に作成したテンプレートは こちら です。以下は一部抜粋です。
{{#operations}}
{{#operation}}
{{#vendorExtensions}}
async fn {{operationId}}(
{{#x-has-path-params}}
path: web::Path<SimplePath>,
{{/x-has-path-params}}
{{#x-has-request-params}}
json: web::Json<BookRequest>,
{{/x-has-request-params}}
db_pool: web::Data<Pool>,
) -> Result<HttpResponse, Error> {
let client: Client = db_pool.get().await.map_err(MyError::PoolError)?;
{{#x-action-create}}
let item = db::{{operationId}}(&client, &json).await?;
let res_body = serde_json::to_string(&item)?;
Ok(HttpResponse::Ok().body(res_body))
{{/x-action-create}}
: (中略)
}
{{/vendorExtensions}}
{{/operation}}
{{/operations}}
{{#operations}}
などの変数を使用して、出力内容を制御します。
operation は、API のパス 1 つ分です。以下のような部分を表します。
/books:
get:
operationId: book_list
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/BookList'
{{#operation}}
と {{/operation}}
の間に {{operationId}}
と書くと、該当パスの operationId が出力されます。
どのような変数が使えるかは、一覧などは見つからなかったので、既存のジェネレータのテンプレートを眺めて想像することにします。
vendorExtensions
どのような変数が使えるのかはよくわかりませんが、変数は自分で定義できます。vendorExtensions を使用します。
変数を書くと何が出力されるのかといえば、それは、Java のソースコードで決めた値が出力されます。
Java のソースコード (ActixWebGenerator.java) は、カスタムジェネレータを作成したときに作られています。
tree out/generators/actix-web/
out/generators/actix-web/
├── src
│ ├── main
│ │ ├── java
│ │ │ └── org
│ │ │ └── openapitools
│ │ │ └── codegen
│ │ │ ├── ActixWebGenerator.java <- これ
例えば ActixWebGenerator.java に以下のようなコードを書いておくと、
@Override
public CodegenOperation fromOperation(String path, String httpMethod, Operation operation, List<Server> servers) {
Map<String, Schema> definitions = ModelUtils.getSchemas(this.openAPI);
CodegenOperation op = super.fromOperation(path, httpMethod, operation, servers);
boolean hasPathParams = !op.pathParams.isEmpty();
op.vendorExtensions.put("x-has-path-params", hasPathParams);
: (中略)
return op;
}
テンプレートに {{#x-has-path-params}}
と書くことができるようになります。
出力結果
ActixWebGenerator.java を編集し、vendorExtensions を駆使することで、テンプレートを完成させます。
async fn book_create(
json: web::Json<BookRequest>,
db_pool: web::Data<Pool>,
) -> Result<HttpResponse, Error> {
let client: Client = db_pool.get().await.map_err(MyError::PoolError)?;
let item = db::book_create(&client, &json).await?;
let res_body = serde_json::to_string(&item)?;
Ok(HttpResponse::Ok().body(res_body))
}
それらしいコードが出力されるようになりました。
上の方で、生成したソースコードのファイルの拡張子が .sample
になっていましたが、ファイル名なども ActixWebGenerator.java で制御します。
おわりに
モデルのソースコードの生成については省略しましたが、ハンドラーとモデルの部分のソースコードを OpenAPI から出力して、Actix-Web のアプリケーションを起動し、動かすことができました。
いろいろ断念した部分はありつつも、openapi-generator は多くの開発言語に対応しているだけあって、工夫次第で望むソースコードの出力ができるようになりそうです。
社内でソースコードの自動生成の手段に何が採用されていくのかはこれからの議論しだいですが、検討材料の一つになるとよいと思います。