connexionを使ってPython APIサーバのAPI定義と実装を関連付ける

# connexion

connexionはOASを利用してAPIエンドポイントとpython関数をmapするためのpythonパッケージです。
Flaskベースで作成されており、通常flaskのdecorationで `@app.route("/")` などとエンドポイントを指定してHTTPリクエストと関数の関係を記述していたものを、OAS側に記載することができます。

何が嬉しいかというと、個人的にはAPI定義を元にルーティングが決定されるためAPI定義と実装の乖離が生じない点だと思います。あとはAPI定義に書いてあればValidationとかもしてくれるみたいです。

connexionは基本的にはFlaskで動作しますが他のpython webサーバでも動作します。
2019年4月現在、次のサーバが利用できるようです。

- flask
- tornado
- aiohttp
- gevent

https://github.com/zalando/connexion

# 使ってみる

## 環境

```bash
$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.3
BuildVersion:   18D109
$ python --version
Python 3.7.2
```

## setup

repositoryに入っているexampleを動かしてみます。
今回はOpenAPI Specification v3.0で記述されたhelloworldを動かします。

```bash
$ git clone https://github.com/zalando/connexion.git
$ cd connexion/examples/openapi3/helloworld/
$ pip install connexion
$ pip show connexion | grep '^Version'
Version: 2.2.0
```

OASの中身は次のようになっています。
APIは1つだけエンドポイント`/greeting/{name}`を持ち、HTTP POSTリクエストを投げると"hello {name}"の文字列が返ってくることが記述されています。
特徴的なのは`operationId`で、この情報をもとにconnexionが呼び出す関数を決定します。

```yaml:openapi/helloworld-api.yaml
openapi: "3.0.0"

info:
  title: Hello World
  version: "1.0"
servers:
  - url: http://localhost:9090/v1.0

paths:
  /greeting/{name}:
    post:
      summary: Generate greeting
      description: Generates a greeting message.
      operationId: hello.post_greeting
      responses:
        200:
          description: greeting response
          content:
            text/plain:
              schema:
                type: string
                example: "hello dave!"
      parameters:
        - name: name
          in: path
          description: Name of the person to greet.
          required: true
          schema:
            type: string
            example: "dave"
```

## 起動

起動してみます。

```bash
$ python hello.py
The swagger_ui directory could not be found.
    Please install connexion with extra install: pip install connexion[swagger-ui]
    or provide the path to your local installation by passing swagger_path=<your path>

The swagger_ui directory could not be found.
    Please install connexion with extra install: pip install connexion[swagger-ui]
    or provide the path to your local installation by passing swagger_path=<your path>

 * Serving Flask app "hello" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:9090/ (Press CTRL+C to quit)
```

この状態で次のようにHTTPリクエストを投げてやると、connexion(Flask)サーバがlocalhost:9090で待ち受けていて期待した通りのResponseが得られます。

```
$ curl -X POST http://localhost:9090/v1.0/greeting/hoge
Hello hoge
```

Warningに書いてあるように、商用環境で利用する際はWSGIサーバを利用してください。
http://flask.pocoo.org/docs/1.0/deploying/

## 中身の確認

`hello.py`の中身は次のようになっています。やっていることは、

1. connexionのFlaskAppインスタンスを作成
2. openapi/helloworld-api.yamlからOASを読み込み
3. サーバ起動
4. operationId: hello.post_greetingのリクエストを捌く関数を定義

```python:hello.py
#!/usr/bin/env python3

import connexion


def post_greeting(name: str) -> str:
    return 'Hello {name}'.format(name=name)

if __name__ == '__main__':
    app = connexion.FlaskApp(__name__, port=9090, specification_dir='openapi/')
    app.add_api('helloworld-api.yaml', arguments={'title': 'Hello World Example'})
    app.run()
```

flaskで書くとこんな感じでしょうか。
APPLICATION_ROOTが違いますが、開発用サーバがROOTを変えられないようなので大目に見てください(WSGI使いましょう)

```python:hello.py
#!/usr/bin/env python3
import flask
app = flask.Flask(__name__)


@app.route('/greeting/<name>', methods=['POST'])
def post_greeting(name: str) -> str:
    return 'Hello {name}'.format(name=name)


if __name__ == '__main__':
    app.run(port=9090)
```

flaskではOASを読みに行かないためAPI定義とソースコード間に関連がないので、API定義を見ながら実装は独立してすることになります。
独立というと耳障りは良いですが実装はAPI仕様を反映しているべきで、そこを自動的に関連付けてくれることがconnexionのメリットなのかなと思います。

# Tips

## デフォルトのhostが違う

FlaskのApp.run()はhostパラメータを指定しなかった場合 `127.0.0.1` でLISTENしますが、connexionは `0.0.0.0` でLISTENします。
大きな違いはないのですが、ちょっと不自然です。issueで質問しているのでわかったら書きます。

Flaskはここでhostパラメータ未指定の際のデフォルト値を指定しています
https://github.com/pallets/flask/blob/master/flask/app.py#L924

一方connexionも独自にデフォルト値を定義しているのでFlaskと違う挙動になっています。
https://github.com/zalando/connexion/blob/master/connexion/apps/flask_app.py#L83

## FlaskAppに他サーバの実装が記述されている

この実装はどうなんだろう🤔

```python:FlaskApp
        if self.server == 'flask':
            (snip)
        elif self.server == 'tornado':
            (snip)
        elif self.server == 'gevent':
```

https://github.com/zalando/connexion/blob/master/connexion/apps/flask_app.py#L94

# まとめ

connexionを動かしてAPIと実装を分離してOASからその関連付けができることを見ました。
まだconnexion自身に怪しい部分がありますが、OASを書くなら使ってみてもいいかもしれません。