Tornado-JSON で手軽に JSON を返す Web API を作ろう


## 前口上
[tornado](https://github.com/tornadoweb/tornado) は軽量な Web Framework と非同期通信ライブラリです。WebSocket やロングポーリングのようなコネクションを長く維持する通信方式を採用している場合に真価を発揮しそうです。
今回は、JSON 形式のレスポンスを返すちょっとしたアプリのサーバー実装のために `Django` のような大きな Web Framework を使いたくなかったので `tornado` を試してみました。

また、`JSONEncorder` を使ってレスポンスを返しても良いのですが、[Tornado-JSON](https://github.com/hfaran/Tornado-JSON) を使えば、レスポンスの形式を [JSend](http://labs.omniti.com/labs/jsend) 仕様にすることができたり、[JSON Schema](http://json-schema.org/) によるバリデーションを行うことができます。また、ドキュメンテーションもできるので、JSON over HTTP で実装したい場合は便利そうです。

## 環境
Web Server: nginx
Language: Python 2.7.10
OS: CentOS release 6.7 (Final)


## インスール

まずは、`tornado` と `Tornado-JSON` を pip でインストールします。

```
pip install tornado
```

```
pip install Tornado-JSON
```

特に問題なくインストールできました。

## demosその１ hellowold

インストールが完了したら、 GitHub から `Tornado-JSON` を clone し、demo を試してみましょう。


```
cd Tornado-JSON/demos/helloworld
python helloworld.py
```

上記を実行すると、tornado アプリケーションが開始されます。下記のように、API の URL と紐づくクラスが見て取れます。


![tornado-json-helloworld-compressed.png](https://qiita-image-store.s3.amazonaws.com/0/54268/41ecfc7a-b303-c299-4b5d-78e9b8663efe.png "tornado-json-helloworld-compressed.png")


では、実際にリクエストしてみましょう。ブラウザからでも良いですが、今回は [httpie](https://github.com/jkbrzt/httpie) を使ってターミナルから確認してみます。
ターミナルに表示されている API を上から順に試していきます。

#### /api/helloworld/?

```
$ http GET <path/to/host>/api/helloworld/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 45
Content-Type: application/json; charset=UTF-8
Date: Thu, 21 Jan 2016 20:22:38 GMT
Etag: "2e708f71c76a9c77119c54d91746619abbbb28fc"
Server: nginx/1.0.15
Vary: Accept-Encoding

{
    "data": "Hello world!",
    "status": "success"
}
```

最も単純な例のようです。

#### /api/freewilled/?

```
$ http GET <path/to/host>/api/freewilled/
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 76
Content-Type: application/json; charset=UTF-8
Date: Thu, 21 Jan 2016 20:27:55 GMT
Etag: "81d9135b038a336645a03c17f8c1f58bf890a010"
Server: nginx/1.0.15
Vary: Accept-Encoding

{
    "data": "I don't need no stinkin' schema validation.",
    "status": "success"
}
```

これは少し分かりにくいのですが、バリデーションをしない例のようです。

#### /api/postit/?

```
# httpie では、生のjsonを送信するために数値型は = ではなく := で指定することに注意。= で指定すると数値が文字列型になってしまい、バリデーションでエラーとなる。 
$ http POST <path/to/host>/api/postit/ title='post it' body='hello world' index:=0
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 65
Content-Type: application/json; charset=UTF-8
Date: Thu, 21 Jan 2016 20:30:53 GMT
Server: nginx/1.0.15
Vary: Accept-Encoding

{
    "data": {
        "message": "post it was posted."
    },
    "status": "success"
}
```

こちらは GET Method ではなく POST Method の例ですね。
title, body, index をパラメータとしている API なので、これらを指定しないと、バリデーションに失敗します。失敗したときは、下記のようになりました。

```
$ http POST <path/to/host>/api/postit/ title='post it' body='hello world' index=0
HTTP/1.1 400 Bad Request
Connection: keep-alive
Content-Length: 179
Content-Type: application/json; charset=UTF-8
Date: Fri, 22 Jan 2016 16:02:19 GMT
Server: nginx/1.0.15
Vary: Accept-Encoding

{
    "data": "u'0' is not of type 'number'\n\nFailed validating 'type' in schema['properties']['index']:\n    {'type': 'number'}\n\nOn instance['index']:\n    u'0'",
    "status": "fail"
}

```

#### /api/greeting/(?P\<fname\>[a-zA-Z0-9_\\-]+)/(?P\<lname\>[a-zA-Z0-9_\\-]+)/?$

```
$ http GET <path/to/host>/api/greeting/John/Smith
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 55
Content-Type: application/json; charset=UTF-8
Date: Thu, 21 Jan 2016 20:45:36 GMT
Etag: "8e58c6953fdb166e2912eca36881f55885f20073"
Server: nginx/1.0.15
Vary: Accept-Encoding

{
    "data": "Greetings, John Smith!",
    "status": "success"
}
```

こちらは正規表現を利用した URL の例ですね。
John, Smith がそれぞれ fname, lname という Key でキャプチャされ、Python プログラムから参照することができます。

#### /api/asynchelloworld/(?P<name>[a-zA-Z0-9_\\-]+)/?$

```
$ http GET <path/to/host>/api/asynchelloworld/Smith
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 78
Content-Type: application/json; charset=UTF-8
Date: Thu, 21 Jan 2016 20:48:19 GMT
Etag: "4f1db48667213b44adbacc2a85599bca86c72f19"
Server: nginx/1.0.15
Vary: Accept-Encoding

{
    "data": "Hello (asynchronous) world! My name is Smith.",
    "status": "success"
}
```

こんにちは、非同期の世界。というわけで、非同期ライブラリを使用した例のようですね。

以上の API 実装は GitHub の [demos/helloworld/helloworld.py](https://github.com/hfaran/Tornado-JSON/blob/master/demos/helloworld/helloworld.py) と [demos/helloworld/helloworld/api.py](https://github.com/hfaran/Tornado-JSON/blob/master/demos/helloworld/helloworld/api.py) にあります。これらを参考にすれば所望の処理が実装できそうです。

## demosその２ rest_api

こちらは URLconf を示す demo です。

helloworld の実装を見ると気づくのですが、URLconf (`Django` でいうところの `urlpatterns`) が見当たりません。
どういうことなのでしょうか。

実は、`APIHandler` に `__urls__`, `__url_names__` という名前のリスト型クラス変数があり、これをオーバーライドすると、URLs を自動生成してくれます。 生成された URLs は `tornado_json.routes.get_routes` 関数で取得します。 これが `Tornado-JSON` の URLconf 作法のようです。

詳しくは、[demos/rest_api/cars/api/\_\_init\_\_.py](https://github.com/hfaran/Tornado-JSON/blob/master/demos/rest_api/cars/api/__init__.py#L20) に記載されています。

## 実際に何か作って公開

ということはありません。（力尽きました。。気が向いたら後日やるかも？

## 試してみた感想
罠は色々あるかもしれませんが、軽量な点が嬉しいです。
一番の魅力だと思われる非同期処理とノンブロッキングIOは詳しく調べられていませんが、特に WebSocket と組み合わせたときにどうなるか興味深いです。

`Tornado-JSON` に関しては、昨今の WEB 開発の要件が、`JSON over HTTP` に移っていることを考えると、魅力的です。ただ、開発があまり活発ではないのか、Contributors も 4人と非常に少ないことに多少不安感があります。一方、`tornado` は比較的活発なので少しは安心感が。

## 参考
`tornado` に関しては、2012年頃に(恐らく唯一の)書籍も出版されているようです。
https://github.com/Introduction-to-Tornado/Introduction-to-Tornado