概要
OpenAPIGeneratorを利用して、Ruby製のFWであるSinatraを利用したAPIの雛形を生成した。
その際、デフォルトだと自動生成されるコードに手を加えないとAPIの内部実装ができないため、生成される雛形をカスタマイズすることで開発しやすい状態にした。
OpenAPIGenerator
OpenAPIGeneratorはOpenAPIの規格にのっとたAPIを開発するためのジェネレーターです。
これを利用してコードを生成することで、RESTFULなAPIを開発するための定義ファイル、APIの雛形を手に入れることができます。
数年前まではSwaggerという名称で呼ばれていましたね。
https://github.com/OpenAPITools/openapi-generator
Sinatra用の雛形をつくってみる
このOpenAPIGeneratorを利用してAPIの雛形を作ってみます。
今回は担当する案件で利用する必要のあった、Ruby製FWのSinatraを利用したAPIを作成します。
インストール
READMEにしたがって行います。
例えばMacならbrew installで入ります。
コード生成
利用するFWや用途に応じたAPIの雛形が生成できます。
Sinatraの場合は下記のようなコマンドで生成します。
openapi-generator generate -i https://raw.githubusercontent.com/openapitools/openapi-generator/master/modules/openapi-generator/src/test/resources/3_0/petstore.yaml -g ruby-sinatra -o ./my_api
my_apiディレクトリ以下に雛形を作成します。
〜petstore.yamlというのはサンプルAPIの雛形ですね。
APIを生成するために定義ファイルが必要になります。
コードを生成する前に自分で定義したい方は下記など見ながら記述し、それを指定してもよいと思います。
https://swagger.io/specification/
まずはサンプルで試しに作成し、サンプルを参考に実装を進めるのもよいと思います。
APIを実装してみる
ジェネレーターを実行するとopenapi.yamlというファイルが生成されます。
このファイルを更新することで、API定義の修正を行うことができ、生成されるコードにも変化が生まれます。
例えば下記のようなAPI定義をしてみます。
・・・
paths:
/prefecture:
get:
operationId: getPrefecture
parameters:
- explode: false
in: query
name: pref_id
required: true
schema:
type: string
style: simple
responses:
"200":
content:
application/json:
schema:
$ref: '#/components/schemas/ApiResponse'
description: successful operation
"400":
description: Invalid status value
summary: 都道府県情報を返却します
こちらを利用してジェネレータを実行します。
openapi-generator generate -i ./my_api/openapi.yaml -g ruby-sinatra -o ./my_api
すると、./my_api/api/以下に下記のようなファイルが出力されます。
require 'json'
MyApp.add_route('GET', '/prefecture', {
"resourcePath" => "/Default",
"summary" => "都道府県情報を返却します",
"nickname" => "get_prefecture",
"responseClass" => "ApiResponse",
"endpoint" => "/prefecture",
"notes" => "",
"parameters" => [
{
"name" => "pref_id",
"description" => "",
"dataType" => "String",
"allowableValues" => "",
"paramType" => "query",
},
]}) do
cross_origin
# the guts live here
{"message" => "yes, it worked"}.to_json
end
openapi.yamlに定義した情報から上記のコードが生成されます。
これでyamlに定義した内容でAPIを呼び出す下地はできるため、# the guts live here 以下の部分にコードを実装していけばいいわけです。
ちなみにyamlにtagsを設定すると、そこに設定した命名でapi以下のファイルが切られるので、defalut以外でファイルを生成したい場合はtagsを設定してください。
上記の問題点
しかし、このままでは実際に開発を行うには大きな支障を抱えた状態になります。
実際に処理を記載していくapi/default_api.rbが自動生成で作成されるファイルであるため、openapi.yamlに手を加え、コードを再生成するたびにコードが初期化されてしまうのです。
GenerationGapパターン
これを回避するためのデザインパターンがあります。
GenerationGapパターンというデザインパターンで、自動生成するコードには手を加えずに開発を進めるためのデザインパターンです。
自動生成するコードではスーパークラスを作成するようにし、それを継承したサブクラスを実装することで、自動生成するコードと開発するコードを分離しようというアイデアです。
GenerationGapパターンを適用する
早速先ほどのコードを下記のように書き換えてみます。
require 'json'
class AbstractDefaultApi
def get_prefecture(params)
{"message" => "yes, it worked"}.to_json
end
end
MyApp.add_route('GET', '/prefecture', {
"resourcePath" => "/Default",
"summary" => "都道府県情報を返却します",
"nickname" => "get_prefecture",
"responseClass" => "ApiResponse",
"endpoint" => "/prefecture",
"notes" => "",
"parameters" => [
{
"name" => "pref_id",
"description" => "",
"dataType" => "String",
"allowableValues" => "",
"paramType" => "query",
},
]}) do
cross_origin
# the guts live here
DefaultApi.new.get_prefecture(params)
end
元のコードでは実際の処理をadd_routeに渡されたblockに記述しなければいけませんでしたが、そこをDefaultApiクラスのget_prefectureメソッドを呼び出すようにしています。
また、同一ファイル内にAbstractDefaultApiクラスを定義し、get_prefectureメソッドを定義しています。
処理を上書きたい場合は、このクラスを継承したクラスでオーバーライドすればよいわけです。
例えば、上記クラスを継承したクラスをresource以下に定義したとします。
require_relative '../api/default_api'
class DefaultApi < AbstractDefaultApi
def get_prefecture(params)
{"message" => "override get_prefecture"}.to_json
end
end
上記を呼び出すようにするため、同様に自動生成されるmy_app.rbにも手を加えます。
diff --git a/my_api/my_app.rb b/my_api/my_app.rb
index 47bc4d8b..c4d37b4d 100644
--- a/my_api/my_app.rb
+++ b/my_api/my_app.rb
@@ -8,6 +8,6 @@ class MyApp < OpenAPIing
end
# include the api files
-Dir["./api/*.rb"].each { |file|
+Dir["./resource/*.rb"].each { |file|
require file
}
呼び出すファイルをapi以下からresource以下に変更しました。
これにより、自動生成されるコードに手を入れることなく、コードを開発する構成が実現できました。
OpenAPIGeneratorで生成される雛形をカスタマイズする
では、OpenAPIGeneratorで上記のようなコードを生成するように変更するにはどうすればよいでしょうか。
OpenAPIGeneratorには雛形に利用するテンプレートの差し替えオプションが存在します。
https://github.com/OpenAPITools/openapi-generator/blob/master/docs/templating.md
要件次第で、より複雑なカスタマイズを行うことも可能なようですが、今回は単純なテンプレートの差し替えで対応を行います。
Sinatraのデフォルトテンプレートは下記にありますので、これを先ほど記載したように変更したものを作成します。
https://github.com/OpenAPITools/openapi-generator/tree/150e24dc553a8ea5230ffb938ed3e6020e972faa/modules/openapi-generator/src/main/resources/ruby-sinatra-server
mkdir /path/to/my_api/template
cp -r openapi-generator/modules/openapi-generator/src/main/resources/ruby-sinatra-server /path/to/my_api/template/
コピーしたテンプレートを下記のように変更します。
require 'json'
{{#operations}}
class Abstract{{classname}}
{{#operation}}
def {{nickname}}(params)
{"message" => "yes, it worked"}.to_json
end
{{/operation}}
end
{{/operations}}
{{#operations}}
{{#operation}}
MyApp.add_route('{{httpMethod}}', '{{{basePathWithoutHost}}}{{{path}}}', {
"resourcePath" => "/{{{baseName}}}",
"summary" => "{{{summary}}}",
"nickname" => "{{nickname}}",
"responseClass" => "{{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}void{{/returnType}}",
"endpoint" => "{{{path}}}",
"notes" => "{{{notes}}}",
"parameters" => [
{{#queryParams}}
{
"name" => "{{paramName}}",
"description" => "{{description}}",
"dataType" => "{{{dataType}}}",
{{#collectionFormat}}
"collectionFormat" => "{{collectionFormat}}",
{{/collectionFormat}}
{{^isContainer}}
"allowableValues" => "{{{allowableValues.values}}}",
{{/isContainer}}
{{#defaultValue}}
"defaultValue" => "{{{defaultValue}}}",
{{/defaultValue}}
"paramType" => "query",
},
{{/queryParams}}
{{#pathParams}}
{
"name" => "{{paramName}}",
"description" => "{{description}}",
"dataType" => "{{{dataType}}}",
"paramType" => "path",
},
{{/pathParams}}
{{#headerParams}}
{
"name" => "{{paramName}}",
"description" => "{{description}}",
"dataType" => "{{{dataType}}}",
"paramType" => "header",
},
{{/headerParams}}
{{#bodyParams}}
{
"name" => "body",
"description" => "{{description}}",
"dataType" => "{{{dataType}}}",
"paramType" => "body",
}
{{/bodyParams}}
]}) do
cross_origin
# the guts live here
{{classname}}.new.{{nickname}}(params)
end
{{/operation}}
{{/operations}}
require './lib/openapiing'
# only need to extend if you want special configuration!
class MyApp < OpenAPIing
self.configure do |config|
config.api_version = '{{version}}'
end
end
# include the api files
Dir["./resources/*.rb"].each { |file|
require file
}
diffで見ると下記になります。
diff openapi-generator/modules/openapi-generator/src/main/resources/ruby-sinatra-server/api.mustache /path/to/my_api/ruby-sinatra-server/api.mustache
3a4,15
> class Abstract{{classname}}
>
> {{#operation}}
> def {{nickname}}(params)
> {"message" => "yes, it worked"}.to_json
> end
>
> {{/operation}}
> end
> {{/operations}}
>
> {{#operations}}
59c71
< {"message" => "yes, it worked"}.to_json
---
> {{classname}}Api.new.{{nickname}}(params)
11c11
< Dir["./api/*.rb"].each { |file|
---
> Dir["./resources/*.rb"].each { |file|
今回利用した変数は下記です。
classname
tagsに設定した名前+Apiの文字列がUpper camel caseで取得できます。
今回は指定してないのでDefaultApiが入ります。
operations
API定義ごとの処理です。今回はAPI定義を1つしか行っていませんが、複数存在する場合にはその数分の定義が書き込まれます。
nickname
operationIdに定義したものがsnake caseで取得できます。定義ごとにユニークな文字列が取得できそうだったのでメソッド名に採用しています。
変数に関しては下記のドキュメントが参考になりそうです。
https://github.com/OpenAPITools/openapi-generator/wiki/Mustache-Template-Variables#mustache-template-variables-in-the-operation
カスタマイズした雛形を利用した開発
最後に上記のカスタマイズしたテンプレートを利用してコードを生成してみます。
openapi-generator generate -i ./my_api/openapi.yaml -t ./my_api/template/ruby-sinatra-server -g ruby-sinatra -o ./my_api
上記のコマンドで先ほど定義したテンプレートによるコードが生成されたのが確認できたと思います。
あとは、resources以下に自動生成したコードを継承して開発を進めてください。
まとめ
OpenApiGeneratorを利用してSinatra用のコードを自動生成する方法、またそのカスタマイズ法について記載しました。
他のAPIについても利用できる方法かもしれませんので、参考になればと思います。
PR
不動産売却の時は LIFULL HOME'S へ!