つばくろぐ @takamii228

知は力なり

Flutterで使うAPI関連のDartコードをSwagger/OpenAPIで自動生成する

はじめに

Flutterの開発の中でSwagger/OpenAPIのYamlからコードを自動生成して使うことがあったのでまとめておきます。

Swagger/OpenAPIとは

OpenAPIはREST APIの仕様を記述するフォーマットで、yaml形式で仕様を定義することができます。

2.0系と3.0系があって、3.0が出てからしばらく立ちましたが、API Gatewayはまだ2.0系のみサポートしているところが多い印象です。

www.openapis.org

swagger.io

SwaggerとOpenAPIの関係は少しややこしいようで、加えて2.0系と3.0系で仕様に差分があるため、ここでは2.0系の形式を扱うこととします。

Swagger/OpenAPIには以下の3つのツールがあります。

今回はSwagger/OpenAPIのyamlを使ってAPI仕様書を見る手順と、Flutterで使うDartのコードを生成する手順をまとめます。

Swagger UIでAPI仕様書を見る

Swagger UIはDocker Imageが用意されているので、yamlを連携してあげれば簡単に起動することができます。

API_URLでデフォルトで読み込むyamlを指定できるので、マウントした領域を読み込んだり、ネットワーク越しにしていすればよいでしょう。

$ docker run -d -p 81:8080 -v $HOME/git/swagger-dart-client/:/usr/share/nginx/html/yaml \
    -e API_URL=yaml/petstore.yaml swaggerapi/swagger-ui

http://localhost:81 にアクセスすれば確認できます。

f:id:takamii228:20200506121406p:plain

Swagger CodegenDartのコードを自動生成する

FlutterでAPIアクセスをして画面を描画する時、APIのモデルデータの一式を定義するのは大変です。

PHPJavaScriptであれば単なるArrayのMapで処理するだけなのですが、JavaDartではきっちり型定義が必要なのでList型や入れ子JSONを含む複雑なレスポンスを返すAPIの場合は定義がとても面倒です。

加えて、特にフロントエンドとバックエンドが並行して新規開発するケースでは、やっとの思いでモデルクラスを定義しきれたとしてもAPIの仕様が流動的なため頻繁な変更が入ります。Typoでミスが入り、バックエンドと繋ぐときにIFの不整合が多発する要因にもなります。

そこでSwagger/OpenAPIのyamlで仕様を定義し、クライアントコードを自動生成するという戦略をとることでその手間やリスクを減らすことがことできます。

ツールをインストールする

コードの自動生成ツールをは、Swagger/OpenAPIそれぞれ用意されているのですが今回はOpenAPIの方を利用します。

インストールは公式のガイドに従っていけばよいです。

openapi-generator.tech

npm、homebrew、docker、jar、bashの5種類用意されています。

それぞれが利用しやすいものを選べばよいと思いますが、今回はjarを利用することにします。

API定義のYamlを記述する

まず YamlAPIの仕様の肝となり、プロダクションで利用するコードの自動生成にも利用するものなので、フロントエンドとバックエンドをつなぐ重要な設計成果物であることをお互いに認識しましょう。

これが非常に重要です。

「ファイルサーバのExcelの方が最新です」ということにならないように、Gitのレポジトリで管理され、かつバージョニングされていることが望ましいです。

Yamlの記述についてはSwagger Editorを使えばSyntaxを確認できますが、yamlが巨大になってくるとブラウザが悲鳴を上げてくるのでcliで実行するとよいでしょう。

$ java -jar openapi-generator-cli.jar validate -i petstore.yaml 
Validating spec (petstore.yaml)
No validation issues detected.

Yamlを手動で記述するのはなかなか大変なので、記述しやすい別の設計ドキュメントから自動生成するようにしてもいいかもしれませんね(何とはいいません🙊🙊🙊)。

個人的にはバックエンドのソースコードからYamlを生成してもらえると実装と設計の一意性がより担保できると思ってます。

YamlからDartのコードを自動生成する

Yamlが出来上がったらあとはクライアントコードの自動生成をするだけです。

Flutterはdartで記述するのでdartのクライアントコードを生成するのですが、GeneratorのREADMEを読むとdartdart2とあります。

Flutterが利用するdartのバージョンは2.x系なのでdart2を利用すればいいのねと思ってコマンドを実行したら

$ java -jar openapi-generator-cli.jar generate -i petstore.yaml -g dart2 -o build
Can't load config class with name 'dart2'
...
[error] Check the spelling of the generator's name and try again.

となってエラーで失敗しました。まだコマンドラインツール上ではdart2は対応していないようです。

githubを見に行くとdart2用のテンプレートは用意されているようなのでテンプレートを指定する形で利用すればよさそうです。

github.com

テンプレートは-tオプションで指定することができます。

$ java -jar openapi-generator-cli.jar generate -i petstore.yaml -g dart -t template/dart2 -o build
[main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
[main] INFO  o.o.c.ignore.CodegenIgnoreProcessor - No .openapi-generator-ignore file found.
[main] INFO  o.o.codegen.DefaultGenerator - OpenAPI Generator: dart (client)
[main] INFO  o.o.codegen.DefaultGenerator - Generator 'dart' is considered stable.
[main] INFO  o.o.c.languages.DartClientCodegen - Environment variable DART_POST_PROCESS_FILE not defined so the Dart code may not be properly formatted. To define it, try `export DART_POST_PROCESS_FILE="/usr/local/bin/dartfmt -w"` (Linux/Mac)
[main] INFO  o.o.c.languages.DartClientCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
[main] INFO  o.o.c.languages.DartClientCodegen - Dart version: 2.x
[main] INFO  o.o.codegen.DefaultGenerator - Model Pets not generated since it's an alias to array (without property) and `generateAliasAsModel` is set to false (default)
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/lib/model/error.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/test/error_test.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/doc/Error.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/lib/model/pet.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/test/pet_test.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/doc/Pet.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/lib/api/pets_api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/test/pets_api_test.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/doc/PetsApi.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/pubspec.yaml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api_client.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api_exception.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api_helper.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/api.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/authentication.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/http_basic_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/api_key_auth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build//lib/auth/oauth.dart
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/git_push.sh
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.gitignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/README.md
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.travis.yml
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.openapi-generator-ignore
[main] INFO  o.o.codegen.AbstractGenerator - writing file /Users/takami228/git/swagger-dart-client/build/.openapi-generator/VERSION

生成コードをカスタマイズする

OpenAPIを使ったコード生成では何も指定しないとAPIクライアント、ドキュメント、テストコードなどが一式生成されます。

モデルクラスのみあればよいというケースでは、オプションを設定することで自動生成をスキップできます。

$ java -jar openapi-generator-cli.jar -DbrowserClient=false -DapiTests=false -DmodelTests=false \
  generate -i petstore.yaml -g dart -t template/dart2 -o build

詳細は Selective Generation を見るとよいでしょう。

また自動生成されたコードがDartのLintルールに沿ってない場合は、テンプレートのmustacheファイルをいじってルールに沿うように修正するとよいです。もしテンプレートをいじる場合は忘れずにテンプレートファイルをバージョン管理しましょう。

一式の生成の流れがまとまったらレポジトリにまとめたり、API仕様書が変更されたときにFlutterのプロジェクトへどう修正を取り込むのかのパイプラインを考えるとよいでしょう。

Flutterの場合はpubspec.yaml経由でバージョンを指定してimportすることもできるようです。やや古いですが、こちらの記事が参考になります。

まとめ

以上がSwagger/OpenAPIのYamlからコードを生成する流れになります。githubにサンプルもまとめておきました。

頻繁に変更されるAPI定義のモデルクラスの修正は、単純でありつつもミスがゆるされない作業になるため、自動生成の仕組みを上手く利用して効率化するとよいでしょう。

github.com

おまけ

Swaggerを使ったコードやドキュメントの自動生成の話はint128先生のJavaのSpringを使った例も参考になるので見てみてください。

Swaggerのテンプレートを魔改造した話 / Customize Swagger Templates - Speaker Deck