Оверинженеринг при документуванні ViewSets Django REST Framework

Трапляється в нашому житті, шановні колеги, що хочеш зробити простіше, а виходить як у новачка. І, що цікаво, існує не мало потужних інструментів, які пропонують просте рішення в обмін на душу. Я маю на увазі, що ціна абстракції буває відповідає красі її використання. Для мене прикладом такого нерівноцінного обміну став Django Rest Framework 3.4.0, його механізм ViewSets і необхідність вивести детальну документацію по розроблюваним API.

Почнемо з простого: мій улюблений формат роботи з DRF — писати тільки APIView нащадків. З одного боку, це повторюваний код, а з іншого — цілком лаконічне рішення з прогнозованим і керованим юзкейсом. По-перше, з ймовірністю 95%, ми не будемо вішати на один эндпоинт кілька сериалазеров. По-друге, ми можемо точніше налаштувати прив'язку URL. Але, з часом починаєш замислюватися: а чи все я зробив правильно? Може, пора відійти від ідеї перевіреного роками REST консерватизму? Тим більше, що DRF має досить непоганий шар абстракції: ViewSets.

Ідея ViewSets проста: у нас є площа модель, і нам не треба вигадувати свої эндпоинты або описувати їх окремими класами. Досить одного класу, який самостійно реєструє views, проводить прив'язку urls і т. д. тобто це дуже багато шаблонів, запакованих в коробочку, повязанную блакитною стрічкою. Завдання стояло щодо стандартна:

1. Є кастомный профіль користувача.
2. У нього є додаткові поля.
3. При реєстрації ми використовуємо REST і вручну визначаємо, які поля обов'язкові, а які ні (override полів моделі на рівні DRF).
4. Логін генерується автоматично.
5. У профілю є зв'язок з инвайтом, а інвайт пов'язаний з організацією, яка цей інвайт виписала.

Після деякого роздуми було вирішено зробити 2 або 3 сериалайзера. Абсолютно точно йде окремий сериалайзер на create. Окремий — на view. Можливо, але не факт, що знадобиться третій — на update (change). Класична схема REST додатки виглядала б так:

serializers.py

class UserCreateSerializer(serializers.ModelSerializer):
pass


class UserViewSerializer(serializers.ModelSerializer):
pass


class UserUpdateSerializer(serializers.ModelSerializer):
pass


views.py

class UserCreateView(APIView):
pass


class UserDetailsView(APIView):
pass


class UserUpdateView(APIView):
pass


Після невеликого рефакторінгу, ми можемо отримати один APIView:

views.py

class UserApiView(APIView):

def get(self, request, *args, **kwargs):
return self.__list_view(request) if 'pk' not in self.kwargs else self.__detail_view(request)

def post(self, request, *args, **kwargs):
return self.__create_view(request) if 'pk' not in self.kwargs else self.__update_view(request)


Як бачите, особливої потреби у ViewSet немає. Трэйс запиту відбувається рівно одним рядком, але нам доступні функції get, post, put і іже з ними. До того ж, якщо нам раптом не сподобається результат, ми завжди зможемо повернутися до формату трьох окремих класів эндпоинтов. У цього методу є ще один плюс: коли ви ставите додаток для автоматичної документації (Swagger або DRF Docs), то отримуєте передбачуваний висновок: або три эндпоинта, або один эндпоинт з трьома описаними методами.

Однак, давайте перейдемо до абстракції ViewSet:

views.py

class UserViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
serializer_classes = {
'list': UserViewSerializer,
'get': UserViewSerializer,
'create': UserCreateSerializer,
'update': UserUpdateSerializer,
'set_password': UserEditSerializer, # воно Нам треба?
'activate': UserEditSerializer
}

def list(self, request, *args, **kwargs):
serializer_class = self.serializer_classes['list']
pass

def create(self, request, *args, **kwargs):
serializer_class = self.serializer_classes['create']


По-перше, впадає в очі велика кількість коду. Його набагато більше, ніж у варіанті з одним эндпоинтом і трьома методами. По-друге, бачимо досить приємну декларативність коду, яка, до речі, нам скоро доведеться ножем по попі.

Отже, наша проблема полягає в тому, що Swagger і DRF Docs не будуть працювати з цим вьюсетом правильно.

Я не рився в коді Swagger, але, думаю, не погрішу, якщо скажу, що він отримує методи эндпоинта так:

1. Get urlpattern
2. Endpoint = urlpattern.callback
3. Methods = endpoint.available_methods

Зверніть увагу на той факт, що callback запитується без створення инстанса, або звернення до методу as_view, який отримує аргументом request. Давайте перевіримо нашу теорію:

views.py

class UserViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
serializer_classes = {
'list': UserViewSerializer,
'get': UserViewSerializer,
'create': UserCreateSerializer,
'update': UserUpdateSerializer,
'set_password': UserEditSerializer, # воно Нам треба?
'activate': UserEditSerializer
}

def get_serializer_class(self):
logger.warn(self.request)
logger.warn(self.actions)
return UserViewSerializer

# Actions here...


Ми отримаємо 500 помилку з інформацією про те, що об'єкт UserViewSet не має атрибуту request. Якщо ми приберемо проблемну позицію, то отримаємо другу помилку: цей об'єкт не має атрибуту actions. Так відбувається тому, що ViewSetMixin виставляє actions при наявності request, хоча логічніше було б зробити список доступних actions у вигляді classproperty (адже при спадкуванні міксина стандартні дії закріплюються по імені та умовами спрацьовування).

Але зараз нас не цікавить, що було б, якби у бабусі були мудики (словник Даля, якщо не помиляюся). У нас є інтерфейс, який можна задокументувати. От прикрість!

Задокументувати інтерфейс на Swagger у мене не вийшло. Милиця вирішення проблеми криється в тому самому методі get_serializer_class(), який ви бачили в попередньому сніппеті. І Swagger, і DRF Docs використовують його, щоб отримати поточний сериалайзер. Ми можемо припустити, що наш код повинен виглядати так:

views.py

class UserViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
serializer_classes = {
'list': UserViewSerializer,
'get': UserViewSerializer,
'create': UserCreateSerializer,
'update': UserUpdateSerializer,
'set_password': UserEditSerializer, # воно Нам треба?
'activate': UserEditSerializer
}

def get_serializer_class(self):
return self.serializer_classes.get(self.action, UserViewSerializer)

# Actions here...


Але ми пам'ятаємо, що на момент спрацьовування get_serializer_class, self.action не існує як атрибута. Це викликає 500 помилку і не дозволяє використовувати даний кейс. Вивчивши обидва рішення (Swagger, DRF Docs), я зупинився на останньому. І тут же отримав ще одну проблему:

сьогодні 27 липня 2016 року, і код DRF Docs з гілки майстра відрізняється від коду DRF Docs, який ставиться через pypi або шляхом завантаження сховища GIT.

Не знаю, глюк це, але, мабуть, git віддає код, зазначений як реліз 0.0.11, а розробники мали зухвалість оновити майстер без релізу. Fail!

Проблема поки вирішується милицею — підміною api_endpoint.py у пакеті. Ви прекрасно розумієте, що це не варіант. Тут у мене два шляхи розвитку коду: або я дочекаюся, поки розробники выкатят новий реліз, або повернуся до варіанту успадкування від APIView. Сьогодні вже немає часу і сил це робити. Розбираючись з кодом цього файлу (який робочий — майстра), я натрапив на два цікавих фрагментв. Ось перший з них:

api_endpoint.py

def __get_serializer_class__(self):
if hasattr(self.callback.cls, 'serializer_class'):
return self.callback.cls.serializer_class

if hasattr(self.callback.cls, 'get_serializer_class'):
return self.callback.cls.get_serializer_class(self.pattern.callback.cls())


Справа в тому, що наша реалізація ViewSet буде завжди містити property serializer_class = None. Логічно було б поміняти перевірку місцями, щоб в пріоритеті дослідити динамічну зміну сериалайзера.

Другий момент:

api_endpoint.py

view_methods = [force_str(m).upper() for m in self.callback.cls.http_method_names if hasattr(self.callback.cls, m)]
return viewset_methods + view_methods


От якщо ви увіткнете стопер між цими двома рядками і спробуєте отримати self.callback.actions, то ви отримаєте той словник, якого нам не вистачає для роботи. Звичайно, тут можна було підключитися до розробки і додати окрему логіку для документування actions… але воно нам даром не треба. Зараз я чекаю від розробників DRF Docs прийняття issue з першою проблемою (serializer_class = None) і сподіваюся на швидкий реліз. Якщо його не трапляється, повертаюся до варіанту з APIView. Що ж стосується методу отримання сериалайзера, то виглядає він так:

views.py

class UserViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):
serializer_classes = {
'list': UserViewSerializer,
'get': UserViewSerializer,
'create': UserCreateSerializer,
'update': UserUpdateSerializer,
'set_password': UserEditSerializer, # воно Нам треба?
'activate': UserEditSerializer
}

def get_serializer_class(self):
if not hasattr(self, 'action'):
action = 'create' if 'POST' in self.allowed_methods else 'list'
else:
action = self.action
return self.serializer_classes.get(action, UserViewSerializer)

# Actions here...


Залишається сподіватися, що метод update не створить проблем при додаванні. Ще одна невелика ремарка: мені довелося все-таки додати метод post:

views.py

class UserViewSet(mixins.ListModelMixin, mixins.CreateModelMixin, mixins.UpdateModelMixin, viewsets.GenericViewSet):

#...

def post(self, *args, **kwargs):
return super().post(*args, **kwargs)

# Actions here...


Без нього DRF Docs не зміг отримати allowed_methods, так і у Swagger були проблеми.

Ось так, шановні колеги, при зверненні до високого рівня абстракції фрэймворка, я зіткнувся з проблемою архітектурної. Зводиться вона до простого висновку: «Винен сам». Хоча питання, звичайно, спірне, адже ViewSets — інструмент зручний і офіційний. Однак, неозброєним оком видно, що питання реєстрації actions в класі не опрацьований. Звідси і небажання розробників документаторов нормально обробляти actions. Результат ситуації простий: сьогодні легше використовувати окремі API Views, ніж шаблони подань для моделі. Принаймні, у більшості відомих REST движків або фреймворків, які вміють створювати REST, ви, швидше за все, не побачите подібних абстракцій. Та дуже велике питання: чи потрібні вони взагалі?
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.