OpenAPI で仕様を書いたら、それで自動生成したコードを使ってサーバー開発したいですよね。
ktor で書いていたサーバーにあとから OpenAPI を導入した際の手法をご紹介します。
3行で
- open-api-generator で ktor のコードを自動生成できる
- しかし、現状は example レスポンスを返す程度のことしかできない
- template を上書きすることで、実際のサーバーとしても使えるコードを生成できる
open-api-generator について
OpenAPI と open-api-generator
OpenAPI は JSON や YAML で API 仕様を書くための仕様を定義しています。
仕様をあらかじめサーバークライアント間で合意しておくことでスムーズに開発できます。
さらに、 open-api-generator を利用すると、 OpenAPI で記述された仕様をもとにしてドキュメントやコードを自動生成することができます。
型安全にサーバーやクライアントコードを手間なく記述できるのはとても便利です。
コードを自動生成する以上、生成先の言語に応じて多くの generator や関連ドキュメントが存在します。
以下には Kotlin / ktor (サーバーサイド) で利用する場合の話をします。
open-api-generator の導入
open-api-generator を ktor で利用する場合、普通は gradle を使うでしょう。
https://openapi-generator.tech/docs/plugins#gradle を参考にすると、かんたんに自動生成タスクを作成できます。
openApiGenerate {
generatorName = "kotlin-server"
inputSpec = "$rootDir/specs/petstore-v3.0.yaml".toString()
outputDir = "$buildDir/generated".toString()
apiPackage = "org.openapi.example.api"
invokerPackage = "org.openapi.example.invoker"
modelPackage = "org.openapi.example.model"
configOptions = [
dateLibrary: "java8"
]
}
generatorName
に kotlin-server
を指定していることに注意してください。
実際にはここをいろいろ差し替えると kotlin のクライアントや spring のコードを生成できます。
$ ./gradlew openApiGenerate
kotlin-server
さて、 kotlin-server を指定すると自動生成できるのですが、実際にそのコードを使ってサーバーを開発することはできません。
なぜなら、example レスポンスが埋め込まれている状態かつ自動生成されたコードに自分たちで手を加えられるようなエントリポイントが存在しないからです。
実際のコードは https://github.com/OpenAPITools/openapi-generator/tree/master/samples/server/petstore/kotlin-server/ktor にあります。
例として このあたり のコードを見ていただいても、直接 example レスポンスが埋め込まれていることがわかるでしょう。
それで使い物にならないかといえば、きちんと example を設定している仕様をベースに、モックサーバーをサクッと立てるような目的では便利です。
なんと、その目的のためか Dockerfile ごと自動生成してくれます。
しかし、サーバーサイドを開発するには不向きです。
自動生成されたコードに手を加えていくことも不可能ではないですが、せっかく OpenAPI で仕様を管理しているのに、自分たちでサーバーサイドの実装も管理することで二重管理になってしまいます。
そんな要望にも応えるべく、 OpenAPI には自動生成するコードを変更するような仕組みが存在するので、以降ではそれを利用していい感じに手を加えていきます。
自動生成したコードの利用
自動生成したコードを以下の方針で利用できるようにします。
- OpenAPI でリクエストのモデルと API エンドポイントごとのパスを生成する
- Application まわりの設定等は自動生成しない
- API エンドポイントごとの Route を人間が書くコードから呼び出せる
- 人間が書くコードが OpenAPI が自動生成するコードに依存する
実装上は OpenAPI が自動生成するコードを人間が書くコードに依存させることも可能ではありますが、柔軟性に欠けるのでやめておいたほうがいいでしょう。
また、これと同じ意味で、 Application まわりは OpenAPI で定義した仕様と直接関係ない設定も数多く行うことがあるので、自動生成しないことにします。
template の上書き
open-api-generator は mustache 形式のテンプレートファイルを利用してコードを自動生成しています。
kotlin-server の場合、テンプレートは ここにあります。
テンプレートファイルと同じ名前のファイルを自分で準備して、 templateDir
にそのファイルを置いているディレクトリを指定することで、自動生成する際のテンプレートを好きなものに差し替えることができます。
例えば、 template
ディレクトリに Paths.kt.mustache
を置いたとします。
$ ls template
Paths.kt.mustache
openApiGenerate
タスクで templateDir
に指定すると、自分で作成した Paths.kt.mustache
を利用してコードを生成してくれます。
openApiGenerate {
generatorName = "kotlin-server"
inputSpec = "$rootDir/specs/petstore-v3.0.yaml".toString()
outputDir = "$buildDir/generated".toString()
templateDir = "$rootDir/template"
apiPackage = "org.openapi.example.api"
invokerPackage = "org.openapi.example.invoker"
modelPackage = "org.openapi.example.model"
configOptions = [
dateLibrary: "java8"
]
}
あとは、 Paths.kt.mustache
の中身さえいい感じにしておけば、実際に利用可能なコードが生成されます!
template の中身の設定
中身は mustache 形式なので、それに沿って記載していきます。
雰囲気で読んでもなんとなくわかる感じのテンプレートにはなっています。
ここで大事なこととして、テンプレートに埋め込めるのはどんな変数なのか、ということがあります。
私は、 openApiGenerate
に verbose = true
を記述してでてきたログに記載されている内容を埋め込むという方法をとりました(もっといい方法もありそうなのでご存知の方がいたら教えて下さい)。
例えば、ログの一部が以下のようになっていたとします。
"basePathWithoutHost" : "/api/v1",
"operations" : {
"classname" : "DefaultApi",
"operation" : [ {
"responseHeaders" : [ {
"openApiType" : "string",
"baseName" : "Set-Cookie",
このときは、 {{basePathWithoutHost}}
を埋め込むと /api/v1
を埋め込めます。
また、 {{operations}}
のスコープ内では {{classname}}
を埋め込むと DefaultApi
が埋め込まれます。
本家のコードも参考に、以下のようなテンプレートに改変しました。
(実際のコードを参考にしたい方は こちら )
{{>licenseInfo}}
package {{packageName}}
{{#apiInfo}}
object Paths {
{{#apis}}
const val basePath = "{{basePathWithoutHost}}"
{{#operations}}
{{#operation}}
/**{{#summary}}
* {{summary}}{{/summary}}
* {{#unescapedNotes}}{{.}}{{/unescapedNotes}}
*/
const val {{operationId}} = "$basePath{{path}}"
{{/operation}}
{{/operations}}
{{/apis}}
}
{{/apiInfo}}
こちらを使うと、例えば以下のようなコードが生成されます。
object Paths {
const val basePath = "/api/v1"
const val usersGet = "$basePath/users"
}
このコードは他の箇所から Paths.usersGet
のように呼び出しが容易になっています。
パスは OpenAPI で定義済みのものから自動生成されているので、手動で作成する手間もありませんしタイポなどのリスクもありません。
この発展形として、 OpenAPI で定義したリクエストの型を利用したエンドポイントの自動生成なんかもできます。
事例の紹介のみで恐縮ですが こちら や こちら を参考にしてみてください。
終わりに
open-api-generator をさらっと使った範囲で、所望の自動生成コードを手に入れることができました。
より深く使うには、ファイルを上書きするだけでなく、追加でファイルを作成したいということも発生するかもしれません。
私自身はまだそこまで手を付けられていないのですが、 このあたりが参考になるかもしれません。