# DRF Rack # Библиотека классов расширений для Django REST Framework # # Автор: Николай В. Анохин # Все права защищены. Лицензия: MIT """ Описание класса EnumChoiceField """ from typing import Any from typing import Sequence from typing import Type from django.core.exceptions import ValidationError from django.db import models from net.xeaf.rack.core.core_enum import CoreEnum class EnumChoiceField(models.CharField): """ Поле выбора перечисления со строгой рантайм-валидацией типов """ MAX_DEFAULT_LENGTH = 64 """ Максимальный размер поля """ _enum_cls: Type[CoreEnum] """ Класс перечисления """ def __init__(self, choices: Type[CoreEnum], *args, **kwargs): """ Инициализация :param choices: Класс перечисления CoreEnum :param args: Дополнительные аргументы :param kwargs: Дополнительные параметры """ if not isinstance(choices, type) or not issubclass(choices, CoreEnum): raise TypeError("The 'choices' parameter must be a subclass of CoreEnum.") self._enum_cls = choices kwargs.setdefault('max_length', self.MAX_DEFAULT_LENGTH) kwargs['choices'] = choices.choices() super().__init__(*args, **kwargs) # noinspection unused-parameter def from_db_value[E: CoreEnum](self, value: Any, expression: Any, connection: Any) -> E | str | None: """ Преобразует сырое значение из базы данных в объект CoreEnum :param value: Входящее сырое значение :param expression: Выражение для преобразования значения :param connection: Соединение с базой данных :return: Представление объекта CoreEnum или строка, или None """ return self._normalize_and_validate(value, action_context="read") def to_python[E: CoreEnum](self, value: Any) -> E | str | None: """ Строго преобразует входящее значение в объект CoreEnum при очистке форм или сериализации :param value: Входящее значение :return: Объект CoreEnum или строка, или None """ return self._normalize_and_validate(value, action_context="clean") def validate(self, value: Any, model_instance: Any) -> None: """ Валидация значения поля """ if value is None or value == "": return super().validate(value, model_instance) lookup_value = value.value if isinstance(value, self._enum_cls) else value if lookup_value not in self._enum_cls.values(): raise ValidationError( self.error_messages['invalid_choice'], code='invalid_choice', params={'value': value}, ) return super(models.CharField, self).validate(value, model_instance) def get_prep_value(self, value: Any) -> str | None: """ Преобразует объект CoreEnum обратно в строку перед сохранением в БД """ # Стандартная проверка value = super().get_prep_value(value) # Проверяем строгость типов и ограничений через наш метод resolved = self._normalize_and_validate(value, action_context="save") if isinstance(resolved, self._enum_cls): return resolved.value return resolved def deconstruct(self) -> tuple[str, str, Sequence[Any], dict[str, Any]]: """ Возвращает параметры для сериализации поля :return: Кортеж с параметрами для сериализации поля """ name, path, args, kwargs = super().deconstruct() kwargs['choices'] = self._enum_cls return name, path, args, kwargs def _normalize_and_validate(self, value: Any, action_context: str) -> CoreEnum | str | None: """ Внутренний метод для проверки null/blank ограничений и приведения значения к CoreEnum. Реализует паттерн Fail-Fast для предотвращения работы с некорректными данными. :param value: Входящее сырое значение :param action_context: Контекст выполнения для текста ошибки (e.g., 'read', 'save') :return: Объект CoreEnum, пустая строка или None :raises ValueError: Если значение нарушает правила null/blank или отсутствует в перечислении """ if value is None: if not self.null: raise ValueError(f"Cannot {action_context} null; the field '{self.name}' is not nullable.") return None if value == "": if not self.blank: raise ValueError(f"Cannot {action_context} blank string; the field '{self.name}' does not allow blanks.") return "" if isinstance(value, self._enum_cls): return value return self._enum_cls.from_string(str(value))