pyramid-swagger-routerと一緒にtoyboxのswaggerのvalidationを使ってみる
相も変わらずtoyboxというリポジトリで作業していた。昨日試しに作ってみたswaggerのinput,outputのvalidationを行うものを昔作っていた pyramid-swagger-router と一緒に使ってみるようにしてみた。ココまで来るとそろそろswaggerに対するnormalizer + iteratorみたいなものが欲しくなってくる。
動くサンプルは ここ
使い方
だいたい以下のようなMakefileを使うという感じで察して欲しい(yapfのコメントアウトはASTのmergeに使うredbaronというpackageのバグを踏んでしまっていたので。yapfはgofmtのpython版みたいなもの)。
gen: spec router schema # fmt: # yapf -r -i --style='{based_on_style: chromium, indent_width: 4}' app spec: cp ../swagger/swagger.yml . gsed -i 's/operationId: /operationId: views./g' swagger.yml router: pyramid-swagger-router --driver=./driver.py:Driver --logging=DEBUG ./swagger.yml app schema: swagger-marshmallow-codegen --full --logging=DEBUG ./swagger.yml > app/schema.py run: PYTHONPATH=. python app/__init__.py
このようにするとだいたい以下のようなコードが生成される。 使うswagger specは 昨日のものと同じもの
routes.py
def includeme_swagger_router(config): config.add_route('views', '/') config.add_route('views1', '/add') config.add_route('views2', '/dateadd') config.scan('.views') def includeme(config): config.include(includeme_swagger_router)
views.py
from pyramid.view import ( view_config ) from . import ( schema ) from toybox.swagger import ( withswagger ) @view_config(decorator=withswagger(schema.Input, schema.Output), renderer='vjson', request_method='GET', route_name='views') def hello(context, request): """ request.GET: * 'name' - `{"type": "string", "example": "Ada", "default": "Friend"}` """ return {} @view_config(decorator=withswagger(schema.AddInput, schema.AddOutput), renderer='vjson', request_method='POST', route_name='views1') def add(context, request): """ request.json_body: ``` { "type": "object", "properties": { "x": { "type": "integer" }, "y": { "type": "integer" } }, "required": [ "x", "y" ] } ``` """ return {} @view_config(decorator=withswagger(schema.DateaddInput, schema.DateaddOutput), renderer='vjson', request_method='POST', route_name='views2') def dateadd(context, request): """ request.json_body: ``` { "type": "object", "properties": { "value": { "type": "string", "format": "date" }, "addend": { "minimum": 1, "type": "integer" }, "unit": { "type": "string", "default": "days", "enum": [ "days", "minutes" ] } }, "required": [ "addend" ] } ``` """ return {}
init.pyは自分で書かなければダメ。
# -*- coding:utf-8 -*- from toybox.simpleapi import run def includeme(config): config.include("toybox.swagger") config.include("app.routes") config.scan("app.views") if __name__ == "__main__": run.include(includeme) run(port=5001)
以下の事が自動で行われる
- routingの定義
- viewの内部のformat validation
もちろんroutingができるだけなので内部のviewは自分で実装してあげないとダメ。
diff --git a/examples/swagger2/app/views.py b/examples/swagger2/app/views.py index a531b8c..85b5a96 100644 --- a/examples/swagger2/app/views.py +++ b/examples/swagger2/app/views.py @@ -1,3 +1,4 @@ +from datetime import datetime, timedelta from pyramid.view import ( view_config ) @@ -17,7 +18,7 @@ def hello(context, request): * 'name' - `{"type": "string", "example": "Ada", "default": "Friend"}` """ - return {} + return {'message': 'Welcome, {}!'.format(request.GET["name"])} @view_config(decorator=withswagger(schema.AddInput, schema.AddOutput), renderer='vjson', request_method='POST', route_name='views1') @@ -45,7 +46,9 @@ def add(context, request): } ``` """ - return {} + x = request.json["x"] + y = request.json["y"] + return {"result": x + y} @view_config(decorator=withswagger(schema.DateaddInput, schema.DateaddOutput), renderer='vjson', request_method='POST', route_name='views2') @@ -82,4 +85,13 @@ def dateadd(context, request): } ``` """ - return {} \ No newline at end of file + value = request.json["value"] + addend = request.json["addend"] + unit = request.json["unit"] + value = value or datetime.utcnow() + if unit == 'minutes': + delta = timedelta(minutes=addend) + else: + delta = timedelta(days=addend) + result = value + delta + return {'result': result}
pyramid-swagger-routerについて
昔記事書いていた
生成したmarshmallowのschemaをwrapしてpyramidから使えるようにしてみた
swaggerからmarshmallowのschemaを生成する機能は昔から作っていて、デフォルトでは definitions
部分だけしか見ないのだけれど。--full
というオプションをつけると paths
以下の parameters
や responses
も見るようになっている。ここで生成したschemaをpyramidから使えるようにした。まだpypiにはあげていない。
だいたい以下の様な手順で使う。
$ pip install swagger-marshmallow-codegen $ swagger-marshmallow-codegen swagger.yml --full > schema.py
あとは以下の様な感じでコードを書けばOK。以下が重要。
config.include("toybox.swagger")
- withswaggerで生成されたschemaを渡したdecoratorをつける
from datetime import datetime, timedelta from toybox.simpleapi import simple_view, run from toybox.swagger import withswagger import schema # ./schema.py @simple_view("/", decorator=withswagger(input=schema.Input, output=schema.Output)) def hello(request): return {'message': 'Welcome, {}!'.format(request.GET["name"])} @simple_view("/add", request_method="POST", decorator=withswagger(input=schema.AddInput, output=schema.AddOutput)) def add(request): x = request.json["x"] y = request.json["y"] return {"result": x + y} @simple_view("/dateadd", request_method="POST", decorator=withswagger(input=schema.DateaddInput, output=schema.DateaddOutput)) def dateadd(request): value = request.json["value"] addend = request.json["addend"] unit = request.json["unit"] value = value or datetime.utcnow() if unit == 'minutes': delta = timedelta(minutes=addend) else: delta = timedelta(days=addend) result = value + delta return {'result': result} if __name__ == "__main__": import logging logging.basicConfig(level=logging.DEBUG) run.include("toybox.swagger") run(port=5001)
viewの内部では全てswaggerのspecを満たしたrequsestであることが保証されている。ダメだった場合にはHTTPBadRequestが返る。
例えば以下の様な感じ
$ http POST :5001/add x=10 y=20 HTTP/1.0 200 OK Content-Length: 14 Content-Type: application/json Date: Sat, 18 Mar 2017 18:24:52 GMT Server: WSGIServer/0.2 CPython/3.5.2 { "result": 30 } $ http POST :5001/add x=10 HTTP/1.0 400 Bad Request Content-Length: 107 Content-Type: application/json Date: Sat, 18 Mar 2017 18:24:55 GMT Server: WSGIServer/0.2 CPython/3.5.2 { "code": "400 Bad Request", "message": { "y": [ "Missing data for required field." ] }, "title": "Bad Request" } $ http POST :5001/add HTTP/1.0 400 Bad Request Content-Length: 214 Content-Type: application/json Date: Sat, 18 Mar 2017 18:24:57 GMT Server: WSGIServer/0.2 CPython/3.5.2 { "code": "400 Bad Request", "message": "The server could not comply with the request since it is either malformed or otherwise incorrect.\n\n\nExpecting value: line 1 column 1 (char 0)\n\n", "title": "Bad Request" }
viewへのinputとして渡せるのはswaggerと同様に以下。
- path –
/foo/{name}
みたいなやつ - query –
/?foo=bar
みたいなやつ - header – request header
- body – REST APIなどでjsonをpostしたときのやつ
- form – 普通のPOST
example全体は以下のリンク先。
https://github.com/podhmo/toybox/tree/master/examples/swagger
どうでも良いこと
ところでmarshmallowのvalidationがload(deserialization)時にだけ掛かるということに気づき衝撃を受けた。しょうがないのでserialize後deserializeするみたいなコードになっている。