山pの楽しいお勉強生活

勉強の成果を垂れ流していきます

Django REST Framework 条件に応じてAPI処理を切り替える方法

全然サンプル見つからなかったので誰かに役に立つかも?と、妥協点はあるもののとりあえず実装としては良さそうなのでメモ

やりたかったこと

  • 条件によって特定のAPI処理全体を切り替えたい
    • 複数発生した場合を考えると if hogeflag: みたいな事はやりたくない
  • この条件というのがログインユーザ毎、特定のデータの場合という事ではなく、環境変数や設定ファイルなど動作する環境毎に切り替えたい。
  • 切り替わったら、その環境では選択されなかった処理は一切使用される事はない

具体的な例とか

  • アプリケーションがパッケージ販売されていて、特定の環境では処理を変更したい
  • 拡張機能などがあり、特定の処理を任意で上書きたい

対応方法

概要

DjangoのMiddlewareを使用する。

Middlewareとは、実際の処理の前後に行わせることができる処理です。hookとか言ったりします。
settings.pyMIDDLEWARE = [] と配列として記載する事で設定しますので、実際のアプリケーションで使われている事が確認できるかと思います。認証処理などもここで行っています。

詳細

# extensions.test_middleware
from django.http import JsonResponse
from rest_framework.request import Request

from extensions.hoge import HogeView

class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        response = self.get_response(request)
        return response

    def process_view(self, request, view_func, view_args, view_kwargs):
        # if view_func.__name__ == 'HogeView': # Classで比較する場合
        if request.path == '/api/v1/hogehoge/': # URLPATHで比較する場合
            # DjangoのRequestとDRFのRequestが異なるため変換
            # django.core.handlers.wsgi.WSGIRequest -> rest_framework.request.Request
            req = Request(request)
            res = HogeView.get(HogeView, req)

            # process_viewではdjango.http.Responseを返す必要があり、
            # rest_framework.response.Responseをそのまま返す事ができないため、値を取得して変換している
            return JsonResponse(res.data, status=res.status_code)
        else:
            # Noneを返す事で既存の処理が行われる
            return None
# extensions.hoge.HogeView
from rest_framework.response import Response
from rest_framework.views import APIView
class HogeView(APIView):
    def get(self, request):
        return Response({'aaa': 'bbb'})
# settings.py

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'extensions.test_middleware.SimpleMiddleware'
]

コードの実物に近い形

直接クラス名とか書いていたら、各処理にifと書くのと何にも変わらない。 ので、設定系は全部DBに突っ込んで、DBの内容と合致したら設定に従ってリフレクションで処理を呼ぶようにした。

class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        return self.get_response(request)

    def process_view(self, request, view_func, view_args, view_kwargs):
        extension = Extension.objects.filter(class_name=view_func.__name__).first()
        if extension:
            _cls = getattr(importlib.import_module(extension.module_name), extension.class_name)
            method = getattr(_cls, request.method.lower())
            return JsonResponse(method(_cls, Request(request)).data, status=res.status_code)

没とした案

urls.pyで対応

実装して動作するところまでは確認

詳細

urlpatterns は先勝ち(ドキュメントは調べてない!)なので、既存処理の設定をする前に独自処理のパターンをどこからか読み込んで設定すれば既存処理を上書きする事ができる。

没とした理由

ユニットテストが大変。

実際にはDBに設定が入っている事を想定していたため、urls.py のurlpatternsの設定する所でDBアクセスする必要がある。

  1. エラー処理の対応が必要
    • url.pyはDjangoの基本機能らしく、 python manage.py migration などでも処理が行われるので、migration前にテーブルがあるわけないのでエラー(ProgramingError)となる。
    • exceptして対応する事も可能は可能。(1度は実装した)
  2. ユニットテストでエラーとなる
    • django.test.TestCaserest_framework.test.APITestCase などのフレームワーク提供のTestCaseを使用する場合に@DB等のDBを使用する旨のデコレータをつけろとエラーになる。
    • これは各テストケース内ではなく、↑のTestCaseを利用する事でDjangoの初期化処理が走るため、url.pyも同様に処理され発生する。
    • 回避したとしたとしても、Djangoの初期処理はテスト全体で一回なので、通常処理の場合と今回の対応を行った場合の2パターンを処理する事ができない。
    • あえてやろうとすると以下の感じになる?(机上の空論)
      • urls.py内でテストかどうかを確認してテストだったらDBにアクセスしない
      • 今回の対応を行う場合のユニットテストsetUpClassDjangoを再起動する

参考