GIG

赴くままに技術を。

SwaggerでWeb APIを作る - APIを実装する

この記事はFujitsu Advent Calendarの20日目です。

Swaggerとは?

前のポストではSwaggerでWeb APIを設計し、ドキュメント化、モックサーバの起動について書いた。 改めてSwaggerについて。

  • REST API設計とそのツール群
    • 仕様書(YAML形式)を書くことでそれから機能を作れたり、ドキュメント化して公開するなど仕様の齟齬をなくす
    • メンテされなくなったExcelで書いた仕様書といったものを防げそう
  • REST APIの標準化団体Open API Initiativeが仕様として採用した

Flaskサーバを実装

使うAPI仕様書は、出来合いのPetstore(Simple)のものを使う(Swagger Editorで[File] > [Open Example...]でpetstore_simple.yamlを選択)。その仕様に沿ったサーバ機能の雛形は、同様にSwagger Editorの[Generate Server]から好みのフレームワークを選択することで取得できる。

ディレクトリ構造は以下のようなかたち。 swagger.yamlAPI仕様書。ロジックは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.yamlapp.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されて返却される

github.com

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