Когда я только начинал пользоваться django, самым приятным моментом после ORM, для меня, был пакет django.forms. Теперь django в прошлом — я использую стэк Werkzeug + SqlAlchemy + Jinja2, а иногда даже пытаюсь вместо SqlAlchemy экспериментировать с нереляционными хранилищами данных. Но вот замену django.forms я так и не нашёл. Поэтому решил набросать по-быстренькому что-нибудь своё.
В итоге, я пришёл примерно к следующему описанию. На входе мы имеем данные представленные типом dict, причём ключами этого словарика являются строки, а значениями — строки или другие словарики, той же структуры. Например:
Далее у нас есть некоторые предположения относительно этих данных — какой-то набор правил, который мы будем называть схемой. Теперь нам нужен способ пройтись по всем полям словарика с данными и проверить их значение на правильность, а также привести к нужным типам. Всё просто!
Отсюда вытекают вполне понятные требования к реализации:
* Простой способ описания схем — хочется, чтобы это было наглядно и удобно, то есть декларативно.
* Переиспользование кода — довольно нудно по 10 раз описывать одни и те же схемы.
* Определение схем для вложенных структур данных — и это может понадобиться.
Основные принципы реализации
Базовые принципы
Предполагается, что ошибка валидации данных будет описываться следующим исключением:
Валидация данных, это практически анализ типов данных, поэтому я считаю уместным наследоваться от стандартного исключения TypeError.
Схема будет задаваться ввиде класса, аттрибутами которого будут объекты, описывающие поля. Так как мы хотим описывать вложенные конструкции, то аттрибутами у нас могут быть как объекты строковых полей, так и другие схемы. Вот что получается на первом этапе:
Так как элементом схемы может быть как поле, так и другая схема, я наследовал Field и Schema от общего класса SchemaElement. Это шаблон проектирования composite, он отлично подходит для описания иерархических типов данных.
Также SchemaElement определяет абстрактный интерфейс для валидации — метод validate. Дело в том, что теперь следуя этому интерфейсу, мы можем не различать объекты Field и Schema с точки зрения валидации, для нас это одно и тоже.
Наследники класса Field будут использоваться для описания полей схемы, то есть для обработки строковых значений. Для того, чтобы реализовать алгоритм валидации данных для конкретного поля, нужно просто переопределить метод validate, который будет возвращать правильные и приведённые данные data или выкидывать исключение SchemaValidationError в случае ошибки. Реализация по-умолчания всегда будет выкидывать исключение.
Класс Schema будет использоваться для описания структуры состоящей из полей и другим схем. Код метода validate будет представлен чуть позднее.
Декларативное описание схем
Как я уже говорил, наиболее удачным мне кажется задание схем ввиде класса, аттрибутами которого являются другие объекты Field и Schema. Это называется декларативное описание. Чтобы это реализовать нам понадобиться метакласс для класса-контейнера Schema:
Основная причина почему я использую этот метакласс — это желание сгруппировать все поля схемы вместе и поместить в аттрибут __fields__. Это будет удобно при обработке полей или интроспекции структуры, так как __fields__ не содержит лишнего мусора, как если бы мы каждый раз обходили __dict__.
Если мы создаём класс с именем Schema, то метакласс никак не будет обрабатывать его, если же это другой класс, наследующийся от Schema, то сначала он соберёт в __fields__ все поля суперклассов в порядке справа-налево и потом добавит туда поля текущего класса.
Также я добавил методы __contains__, который будет проверять содержится ли поле с данным именем внутри схемы, и метод __iter__, что делает класс со схемой итерируемым. Напомню, что так как мы опредили эти методы у метакласса, то мы получаем методы класса, что эквивалентно применению декоратора classmethod на методы объекта.
Теперь осталоcь добавить аттрибут __metaclass__ в класс Schema:
Мы уже можем определять схемы следующим образом:
Наследование схем работает — аттрибут my_field появился и у схемы AnotherSchema. Чтобы создать схему для валидации иерархических структур данных, нужно просто добавить аттрибутом схемы другую схему:
Валидация данных
Валидация выполняется методом validate, объекты класса Field должны сами переопредилить его, реализацию же метода validate у класса Schema я привожу тут:
Сначала у каждого поля схемы вызывается метод validate c нужным параметром из словаря data. Если есть ошибка, она ловится и сохраняется в словаре errors. После того, как мы обошли все поля, проверяется словарик errors, и если он не пуст, то выкидывается исключение SchemaValidationError с этим словариком ввиде параметра. Это позволяет нам собрать все ошибки, начиная с самого нижнего уровня в иерархии.
Теперь можно попробовать определить несколько базовых полей и схем и попробовать валидацию данных в действии:
Внутри метода validate мы должны обязательно вызвать метод validate суперкласса. Также обязательно необходимо вернуть data или выкинуть исключение SchemaValidationError. Проверим нашу форму в деле:
Теперь попробуем предоставить на валидацию неверные данные:
Как и предполагалось, валидация данных завершилась ошибкой.
Заключение
И так, мы имеем маленькую, но уже достаточно мощную библиотечку для валидации данных. Конечно необходимо пополнить её необходимыми полями (классами-наследниками Field). Кстати получилось довольно компактно — не более 130 строк. Если есть желание получить исходный код, вы можете написать мне.
В итоге, я пришёл примерно к следующему описанию. На входе мы имеем данные представленные типом dict, причём ключами этого словарика являются строки, а значениями — строки или другие словарики, той же структуры. Например:
data = { "key1": "value1" "key2": { "key3": "value3" } }
Далее у нас есть некоторые предположения относительно этих данных — какой-то набор правил, который мы будем называть схемой. Теперь нам нужен способ пройтись по всем полям словарика с данными и проверить их значение на правильность, а также привести к нужным типам. Всё просто!
Отсюда вытекают вполне понятные требования к реализации:
* Простой способ описания схем — хочется, чтобы это было наглядно и удобно, то есть декларативно.
* Переиспользование кода — довольно нудно по 10 раз описывать одни и те же схемы.
* Определение схем для вложенных структур данных — и это может понадобиться.
Основные принципы реализации
Базовые принципы
Предполагается, что ошибка валидации данных будет описываться следующим исключением:
class SchemaValidationError(TypeError): def __init__(self, error): self.error = error
Валидация данных, это практически анализ типов данных, поэтому я считаю уместным наследоваться от стандартного исключения TypeError.
Схема будет задаваться ввиде класса, аттрибутами которого будут объекты, описывающие поля. Так как мы хотим описывать вложенные конструкции, то аттрибутами у нас могут быть как объекты строковых полей, так и другие схемы. Вот что получается на первом этапе:
class SchemaElement(object): u""" Абстрактный класс для элемента схемы. """ def validate(self, data): raise NotImplementedError() class Field(SchemaElement): u""" Класс Field описывает строковое поле. """ def validate(self, data): raise SchemaValidationError("not valid value") class Schema(SchemaElement): u""" Класс Schema описывает схему валидации. """ def validate(self, data): # Код валидации данных data return data
Так как элементом схемы может быть как поле, так и другая схема, я наследовал Field и Schema от общего класса SchemaElement. Это шаблон проектирования composite, он отлично подходит для описания иерархических типов данных.
Также SchemaElement определяет абстрактный интерфейс для валидации — метод validate. Дело в том, что теперь следуя этому интерфейсу, мы можем не различать объекты Field и Schema с точки зрения валидации, для нас это одно и тоже.
Наследники класса Field будут использоваться для описания полей схемы, то есть для обработки строковых значений. Для того, чтобы реализовать алгоритм валидации данных для конкретного поля, нужно просто переопределить метод validate, который будет возвращать правильные и приведённые данные data или выкидывать исключение SchemaValidationError в случае ошибки. Реализация по-умолчания всегда будет выкидывать исключение.
Класс Schema будет использоваться для описания структуры состоящей из полей и другим схем. Код метода validate будет представлен чуть позднее.
Декларативное описание схем
Как я уже говорил, наиболее удачным мне кажется задание схем ввиде класса, аттрибутами которого являются другие объекты Field и Schema. Это называется декларативное описание. Чтобы это реализовать нам понадобиться метакласс для класса-контейнера Schema:
class SchemaMeta(type): def __new__(mcs, name, bases, attrs): if not name == "Schema": fields = {} for base in reversed(bases): if issubclass(base, Schema) and not base is Schema: fields.update(base.__fields__) for field_name, field in attrs.items(): if isinstance(field, SchemaElement): fields[field_name] = attrs[field_name] attrs["__fields__"] = fields cls = type.__new__(mcs, name, bases, attrs) return cls def __contains__(cls, value): return value in cls.__fields__ def __iter__(cls): return cls.__fields__.items().__iter__()
Основная причина почему я использую этот метакласс — это желание сгруппировать все поля схемы вместе и поместить в аттрибут __fields__. Это будет удобно при обработке полей или интроспекции структуры, так как __fields__ не содержит лишнего мусора, как если бы мы каждый раз обходили __dict__.
Если мы создаём класс с именем Schema, то метакласс никак не будет обрабатывать его, если же это другой класс, наследующийся от Schema, то сначала он соберёт в __fields__ все поля суперклассов в порядке справа-налево и потом добавит туда поля текущего класса.
Также я добавил методы __contains__, который будет проверять содержится ли поле с данным именем внутри схемы, и метод __iter__, что делает класс со схемой итерируемым. Напомню, что так как мы опредили эти методы у метакласса, то мы получаем методы класса, что эквивалентно применению декоратора classmethod на методы объекта.
Теперь осталоcь добавить аттрибут __metaclass__ в класс Schema:
class Schema(SchemaElement): ... __metaclass__ = SchemaMeta ...
Мы уже можем определять схемы следующим образом:
>>> class MySchema(Schema): ... my_field = Field() >>> class AnotherSchema(MySchema): ... another_field = Field() >>> "my_field" in MySchema True >>> "another_field" in AnotherSchema True >>> "my_field" in AnotherSchema True
Наследование схем работает — аттрибут my_field появился и у схемы AnotherSchema. Чтобы создать схему для валидации иерархических структур данных, нужно просто добавить аттрибутом схемы другую схему:
>>> class CompositeSchema(Schema): sub_schema = MySchema() my_field = Field() >>> "my_field" in CompositeSchema True >>> "sub_schema" in CompositeSchema True >>> "my_field" in CompositeSchema.sub_schema True
Валидация данных
Валидация выполняется методом validate, объекты класса Field должны сами переопредилить его, реализацию же метода validate у класса Schema я привожу тут:
class Schema(SchemaElement): ... def validate(self, data): errors = {} for field_name, field in self.__fields__.items(): try: data[field_name] = field.validate(data.get(field_name, None)) except SchemaValidationError, error: errors[field_name] = error.error if errors: raise SchemaValidationError(errors) return data ...
Сначала у каждого поля схемы вызывается метод validate c нужным параметром из словаря data. Если есть ошибка, она ловится и сохраняется в словаре errors. После того, как мы обошли все поля, проверяется словарик errors, и если он не пуст, то выкидывается исключение SchemaValidationError с этим словариком ввиде параметра. Это позволяет нам собрать все ошибки, начиная с самого нижнего уровня в иерархии.
Теперь можно попробовать определить несколько базовых полей и схем и попробовать валидацию данных в действии:
class NotEmptyField(Field): u""" Класс описывающий поле, которое не может быть пустым. """ def validate(self, data): print "Валидация поля" if not data: raise SchemaValidationError("empty field") class CustomSchema(Schema): not_empty_field = NotEmptyField() def validate(self, data): print "Валидацию полей схемы" data = super(CustomSchema, self).validate(data) print "Код валидации на уровне схемы" return data
Внутри метода validate мы должны обязательно вызвать метод validate суперкласса. Также обязательно необходимо вернуть data или выкинуть исключение SchemaValidationError. Проверим нашу форму в деле:
>>> schema = CustomSchema() >>> try: ... schema.validate({ "not_empty_field": "some value" }) ... except SchemaValidationError, e: ... errors = e.error Валидацию полей схемы Валидация поля Код валидации на уровне схемы >>> schema.errors {}
Теперь попробуем предоставить на валидацию неверные данные:
>>> try: ... schema.validate({ "not_empty_field": "" }) ... except SchemaValidationError, e: ... errors = e.error Сначала сделаем валидацию полей схемы Валидация поля >>> errors { "not_empty_field": "empty field" }
Как и предполагалось, валидация данных завершилась ошибкой.
Заключение
И так, мы имеем маленькую, но уже достаточно мощную библиотечку для валидации данных. Конечно необходимо пополнить её необходимыми полями (классами-наследниками Field). Кстати получилось довольно компактно — не более 130 строк. Если есть желание получить исходный код, вы можете написать мне.