SwaggerでWeb APIを作る - APIを実装する
この記事はFujitsu Advent Calendarの20日目です。
Swaggerとは?
前のポストではSwaggerでWeb APIを設計し、ドキュメント化、モックサーバの起動について書いた。 改めてSwaggerについて。
- REST API設計とそのツール群
- REST APIの標準化団体Open API Initiativeが仕様として採用した
Flaskサーバを実装
使うAPI仕様書は、出来合いのPetstore(Simple)のものを使う(Swagger Editorで[File] > [Open Example...]でpetstore_simple.yamlを選択)。その仕様に沿ったサーバ機能の雛形は、同様にSwagger Editorの[Generate Server]から好みのフレームワークを選択することで取得できる。
ディレクトリ構造は以下のようなかたち。 swagger.yamlがAPI仕様書。ロジックはdefault_controller.pyにつくる。
. ├── LICENSE ├── README.md ├── __init__.py ├── app.py ├── controllers │ ├── __init__.py │ └── default_controller.py └── swagger └── swagger.yaml
Swaggerに準拠したFlaskのフレームワークとしては、connexionがある。 hjacobs/connexion-exampleに習い、今回はデータを辞書として持ち回るように実装する。
まずはPythonのライブラリを2つほどインストールする。
$ pip install Flask connexion
次にアプリケーションの起点であるapp.pyを見てみる。
API仕様書のswagger.yaml
をapp.add_api
で読み込み、8080ポートでサーバを起動することが書いてあるのみ。
#!/usr/bin/env python3 import connexion if __name__ == '__main__': app = connexion.App(__name__, specification_dir='./swagger/') app.add_api('swagger.yaml') app.run(port=8080)
default_controller.pyが処理を書くメインとなっている。
このパスとメソッド名をドットでつないだものがoperationId
で指定しているものである。
connexionは、辞書で返却するとjsonで返してくれる(jsonify
などでjson化しなくて良い)。
import logging from connexion import NoContent logger = logging.getLogger(__name__) PETS = {} def add_pet(pet): id = pet['id'] # idがすでに使用されているか exists = id in PETS if exists: logger.info('If exists, update %s', id) PETS[id].update(pet) else: logger.info('If not exists, create %s', id) PETS[id] = pet return PETS[id], 200 def delete_pet(id): if id in PETS: logger.info('Deleting pet %s', id) del(PETS[id]) return NoContent, 204 else: return NoContent, 404 def find_pet_by_id(id): pet = PETS.get(id) return pet or (NoContent, 404) def find_pets(tags=None, limit=None): return [pet for pet in PETS.values() if not tags or pet['tag'] in tags][:limit]
実装したREST APIを使ってみる
- まずはデータのPost。
- レスポンスとして投入したデータを返却する。
$ cat pet.json {"id":1,"name":"foo","tag":"dog"} $ cat pet2.json {"id":2,"name":"bar", "tag":"cat"} $ curl -i -X POST --header "Content-Type: application/json" --header "Accept: application/json" http://localhost:8080/api/pets -d @pet.json HTTP/1.0 200 OK Content-Type: application/json Content-Length: 46 Server: Werkzeug/0.11.11 Python/3.5.2 Date: Mon, 19 Dec 2016 14:02:06 GMT { "tag": "dog", "name": "foo", "id": 1 } $ curl -i -X POST --header "Content-Type: application/json" --header "Accept: application/json" ttp://localhost:8080/api/pets -d @pet2.json HTTP/1.0 200 OK Content-Type: application/json Content-Length: 46 Server: Werkzeug/0.11.11 Python/3.5.2 Date: Mon, 19 Dec 2016 14:02:11 GMT { "tag": "cat", "name": "bar", "id": 2 }
- 一覧を取得
$ curl -i http://localhost:8080/api/pets HTTP/1.0 200 OK Content-Type: application/json Content-Length: 118 Server: Werkzeug/0.11.11 Python/3.5.2 Date: Mon, 19 Dec 2016 14:32:26 GMT [ { "name": "foo", "id": 1, "tag": "dog" }, { "name": "bar", "id": 2, "tag": "cat" } ]
- 一覧を取得(取得件数を1件(limit=1)を指定)
$ curl -i http://localhost:8080/api/pets?limit=1 HTTP/1.0 200 OK Content-Type: application/json Content-Length: 60 Server: Werkzeug/0.11.11 Python/3.5.2 Date: Mon, 19 Dec 2016 14:33:26 GMT [ { "name": "foo", "id": 1, "tag": "dog" } ]
- 一覧を取得(
tags
を指定)dog
のみ取得
$ curl -i http://localhost:8080/api/pets?tags="dog" HTTP/1.0 200 OK Content-Type: application/json Content-Length: 60 Server: Werkzeug/0.11.11 Python/3.5.2 Date: Mon, 19 Dec 2016 14:35:20 GMT [ { "name": "foo", "id": 1, "tag": "dog" } ]
cat
またはdog
のtagのものを取得
$ curl -i http://localhost:8080/api/pets?tags="dog,cat" HTTP/1.0 200 OK Content-Type: application/json Content-Length: 118 Server: Werkzeug/0.11.11 Python/3.5.2 Date: Mon, 19 Dec 2016 14:35:36 GMT [ { "name": "foo", "id": 1, "tag": "dog" }, { "name": "bar", "id": 2, "tag": "cat" } ]
- 削除してから再び取得
- 削除して204となる
- id=1を取得して404となる
$ curl -i -X DELETE http://localhost:8080/api/pets/1 HTTP/1.0 204 NO CONTENT Content-Type: text/html; charset=utf-8 Content-Length: 0 Server: Werkzeug/0.11.11 Python/3.5.2 Date: Mon, 19 Dec 2016 14:37:23 GMT $ curl -i http://localhost:8080/api/pets/1 HTTP/1.0 404 NOT FOUND Content-Type: text/html; charset=utf-8 Content-Length: 0 Server: Werkzeug/0.11.11 Python/3.5.2 Date: Mon, 19 Dec 2016 14:37:43 GMT
その他
レスポンスはpretty printされて返却される
add_api
のオプションとして渡せたらいいなという議論がある。今はflask_compressを使う。
x-swagger-router-controller:
をオペレーションの上に書くとエラー
x-swagger-router-controller
としては、controllerのモジュールのパス(上記の例だとcontrollers.default_controller
を記載して、oeprationId
はメソッドのみを記載するといった使い方ができそうだけど、URL直下に書くと以下のようなエラーが起きる。
Traceback (most recent call last): File "app.py", line 8, in <module> app.add_api('swagger.yaml') File "/Users/hermesian/.anyenv/envs/pyenv/versions/dev352/lib/python3.5/site-packages/connexion/app.py", line 146, in add_api debug=self.debug) File "/Users/hermesian/.anyenv/envs/pyenv/versions/dev352/lib/python3.5/site-packages/connexion/api.py", line 102, in __init__ validate_spec(spec) File "/Users/hermesian/.anyenv/envs/pyenv/versions/dev352/lib/python3.5/site-packages/swagger_spec_validator/validator20.py", line 88, in validate_spec validate_apis(apis, bound_deref) File "/Users/hermesian/.anyenv/envs/pyenv/versions/dev352/lib/python3.5/site-packages/swagger_spec_validator/validator20.py", line 151, in validate_apis oper_params = deref(oper_body.get('parameters', [])) AttributeError: 'str' object has no attribute 'get'
ドキュメントではオペレーション直下に書いているけど、それならoperationId
だけで良さそう。
API仕様書のresponseを実装側で上書きしてしまう
API仕様書ではリターンコードが204だが、アプリケーション側で200を返すように書くと、後者で返される。レスポンスデータについても同様。 これ結構悩ましい問題。レスポンスのバリデーションが必要そう。