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を返すように書くと、後者で返される。レスポンスデータについても同様。 これ結構悩ましい問題。レスポンスのバリデーションが必要そう。