OpenAPI 3 ファーストな Web アプリケーション開発(Python で API 編)
ソフトウェアエンジニアの花岡です。前回触れた設計ファーストなアプローチに関して Python での API 実装について書いてみました。
機能
設計ファーストなアプローチでは、OpenAPI 3 ドキュメントをもとにした以下のような機能を実現したいと考えました。
- リクエストの validation と unmarshaling
- Operation Object で定義した parameters や requestBody をもとに自動でリクエストを validation
- さらに format に応じて unmarshaling(たとえば
date
ならdatetime.date
のインスタンスに変換)
- アクセス制御
- Security Scheme Object と Security Requirement Object で定義したアクセス制御を自動でリクエストに適用
- ルーティングの自動生成
- Path Item Object と REST リソース実装を自動で関連付け
これらの機能はアプリケーションで実装せず、ライブラリから自動で提供されるような形が理想です。
Web フレームワークとライブラリ
Web フレームワークは Falcon です。当時、いいかんじの OpenAPI 3 ライブラリがなかったため、OpenAPI 3 実装とそれを利用した Falcon の OpenAPI 3 ライブラリとして falcon-oas を実装しました。Web フレームワーク非依存の OpenAPI 3 実装は falcon_oas.oas
に分離しています。spec などのネーミングや jsonschema を利用する仕組みは弊社の開発者ブログでも何度か取り上げてきた bravado-core からいただきました。
設計では、アプリケーション全体を支配するオレオレフレームワークにするのではなく、既存の Falcon の世界で利用可能なライブラリを目指しました。リクエストのバリデーションとアクセス制御は Falcon のミドルウェアから提供されます。バリデーションのパスしたリクエスト情報とアクセス制御のパスしたユーザ情報はそれぞれ falcon.Request.context
に格納されます。アプリケーションはそれを使うだけです。他の Falcon の機能も普通に使えます。
以下の例は、OpenAPI 3 と Falcon の知識があるとわかりやすいと思いますが、なくてもなんとなく雰囲気でわかった気になれると思います。
例
paths:
/api/v1/pets/{pet_id}:
get:
responses:
'200':
description: A pet.
delete:
responses:
'204':
description: Successful deletion.
security:
- api_key: []
parameters:
- name: pet_id
in: path
required: true
schema:
type: integer
components:
securitySchemes:
api_key:
type: apiKey
name: X-API-Key
in: header
この OpenAPI 3 ドキュメントの断片は /api/v1/pets/{pet_id}
のパスの GET と DELETE リクエストに対して
pet_id
は integer 型であること- DELETE リクエストは api_key Security Scheme Object により X-API-Key ヘッダが必要なこと
などを定義しています。Falcon の API 実装としては
class PetItem:
def on_get(self, req, resp, pet_id):
pet = get_pet_by_id(pet_id)
resp.media = pet.to_dict()
def on_delete(self, req, resp, pet_id):
if not is_valid_api_key(req.get_header('X-API-Key')):
raise falcon.HTTPForbidden()
delete_pet_by_id(pet_id)
resp.status = falcon.HTTP_NO_CONTENT
api = falcon.API()
api.add_route('/api/v1/pets/{pet_id:int}', PetItem())
のようなかんじになると思います。これが
class PetItem:
def on_get(self, req, resp, pet_id):
pet = get_pet_by_id(pet_id)
resp.media = pet.to_dict()
def on_delete(self, req, resp, pet_id):
pet = delete_pet_by_id(int(pet_id))
resp.status = falcon.HTTP_NO_CONTENT
with open('/path/to/openapi.json') as f:
spec_dict = json.load(f)
api = falcon_oas.OAS(spec_dict).create_api()
のように書けるようになります。falcon_oas.OAS
がエントリポイントで OpenAPI 3 ドキュメントをもとにした falcon.API
インスタンスを生成することができます。その他変わったところは
- Field Converters がなくなった
- falcon-oas が OpenAPI 3 ドキュメントの schema をもとに pet_id を int に変換します
is_valid_api_key
がなくなった- falcon-oas が Security Scheme Object と Security Requirement Object に基づいたチェックをします
falcon.API.add_route
がなくなった- falcon-oas が OpenAPI 3 ドキュメントのパスと REST リソース実装を
falcon.API.add_route
します
- falcon-oas が OpenAPI 3 ドキュメントのパスと REST リソース実装を
以上のように OpenAPI 3 ドキュメントで定義した内容を falcon-oas が処理するため、それらの機能の実装がなくなっただけでその他は普通の Falcon の世界ということがわかると思います。OpenAPI 3 ドキュメントは
paths:
/api/v1/pets/{pet_id}:
x-falcon-oas-implementation: path.to.PetItem
get:
responses:
'200':
description: A pet.
delete:
responses:
'204':
description: Successful deletion.
security:
- api_key: []
parameters:
- name: pet_id
in: path
required: true
schema:
type: integer
components:
securitySchemes:
api_key:
x-falcon-oas-implementation: path.to.api_key_validator
type: apiKey
name: X-API-Key
in: header
のようにしてx-falcon-oas-implementation
拡張で Path Item Object に REST リソース実装を、Security Scheme Object にアクセス制御実装を関連付けます。
以下、実装について少し詳しく説明します。
falcon_oas.OAS
エントリポイントです。デフォルトで上記の機能と RFC 7807 サポートを有効にした falcon.API
インスタンスを生成することができます。
falcon_oas.OAS.create_api
は falcon.API
のすべてのパラメータを受け取ることができます。
リクエストの validation と unmarshaling
Operation Object の parameters と requestBody をもとにリクエストをバリデーションします。
リクエストパラメータは文字列のため、Parameter Object の schema で type が定義されているとそれをもとにパースしてからバリデーションします。そのため上記の例では integer な pet_id の値が int になっています。
さらに format の定義がある場合は値の変換もすることができます。デフォルトでは date
の場合は datetime.date
に変換されます。format のカスタマイズは falcon_oas.OAS
の formats
パラメータで可能です。
Parameter Object の style と explode はまだサポートしていません。しかし d=2020-01-01&d=2020-01-02&d=2020-01-03
のようなクエリ文字列は Falcon が値を ['2020-01-01', '2020-01-02', '2020-01-03']
のリストにパースするため
type: array
items:
type: string
format: date
のような schema を定義していると [datetime.date(2020, 1, 1), datetime.date(2020, 1, 2), datetime.date(2020, 1, 3)]
のリストが得られます。
path パラメータは上記の例の pet_id のように responder のパラメータの値が unmarshal 済みの値になります。
すべての unmarshal 済みのパラメータは falcon.Request.context['oas'].parameters
に dict として格納されるので、req.context['oas'].parameters['path']['pet_id']
でもアクセスできます。
unmarshal 済みの request body は falcon.Request.context['oas'].media
でアクセスできます。
アクセス制御
現在のところ Security Scheme Object の apiKey type のみサポートしています。上記の例の通り x-falcon-oas-implementation
拡張でアクセス制御関数を関連付けます。
アクセス制御関数は
- Security Scheme Object に対応するリクエストパラメータの値
- 上記の例では X-API-Key ヘッダの値
- Security Requirement Object のスコープリスト
- 将来的な oauth2、openIdConnect type サポートのため
- apiKey type のときは常に空のリスト
falcon_oas.oas.Request
のインスタンス- Web フレームワーク非依存な HTTP リクエストの表現
をパラメータとして受け取り、以下のいずれかの値を返す必要があります。
True
- 単にアクセスを許可する場合
True
以外の真値- アクセスを許可するだけでなく、その戻り値を認証済みユーザとする場合
- 戻り値は
falcon.Request.context['oas'].user
でアクセス可能
- 偽値
- アクセスを拒否する場合
- falcon-oas が自動で 403 エラーを返す
たとえば
x-falcon-oas-implementation: path.to.session_cookie_loader
type: apiKey
name: session
in: cookie
のような Security Scheme Object の場合、session_cookie_loader
は以下のようなかんじになります。
def session_cookie_loader(value, scopes, req):
credentials = deserialize_session_cookie(value)
if credentials:
return User(**credentials)
さいごに
OpenAPI 3 ファーストな Web アプリケーション開発の設計ファーストアプローチに関して、API でどのような機能を実現したかったのか、Python でどのように実現したのか説明しました。
弊社では一緒に事業をご牽引いただけるエンジニアを募集しています。
その他の記事
Other Articles
関連職種
Recruit