Budując REST API mogą przydać się walidatory danych wejściowych bardziej elastyczne niż formularze Django. Do realizacji tego celu polecam i używam Colander.
Colander jest świetnym pakietem do (de)serializacji danych. Jest to odpowiednik formularzy Django dla Pyramida, lecz zdecydowanie bardziej elastyczny oraz bez związku HTML. Formularze Django są źle zaprojektowane - zbyt ściśle związane z HTML (widgety), których przecież nie używamy budując REST API, oraz są ograniczone do płaskich struktur. Jedynie jest są dyspozycji formsety, co pozwala zwalidować i zdeserializować listę obiektów jednego typu.
Colander wolny jest od tych wad, a ponieważ nie ma zależności od Pyramid (jest pakietem samodzielnym) to można go użyć z Django bez żadnego problemu. Jednak w niektórych sytuacjach brakuje mi pola typu ModelChoice, co utrudnia fabrykowanie instancji modeli. Z tego powodu zrobiłem poniższy snippet definiujący walidator ModelOneOf
oraz klasę węzła ModelChoice
. Jest na tyle przydatny, że postanowiłem się z nim podzielić jak najszybciej:
import colander
import types
class ModelOneOf(object):
def __init__(self, qs):
self._qs = qs
def __call__(self, node, value):
if not self._qs.filter(pk=value).exists():
raise colander.Invalid(node, '%r is not valid choice' % value)
class ModelChoice(colander.SchemaType):
def __init__(self, qs, *args, **kw):
self._qs = qs
self._model = qs.model
self._validate = ModelOneOf(self._qs)
super(ModelChoice, self).__init__(*args, **kw)
def serialize(self, node, appstruct):
if appstruct is colander.null:
return colander.null
if not isinstance(appstruct, self._model):
raise colander.Invalid(
node, '%r is not a %s' % (appstruct, self._model))
return appstruct.pk
def deserialize(self, node, cstruct):
if cstruct is colander.null:
return colander.null
if not isinstance(cstruct, (types.StringType, int)):
raise colander.Invalid(
node, '%r is not a string nor int' % cstruct)
self._validate(node, cstruct)
return self._qs.get(pk=cstruct)
Przykładowe użycie (forms.py
):
import colander
from .models import MyModel
class MySchema(colander.MappingSchema):
model = colander.SchemaNode(ModelChoice(MyModel.objects.all()))
In [1]: s = MySchema()
In [2]: s.deserialize({'model': '1'}) # gdzie '1' to wartość PK
Out[2]: {'model': <MyModel pk=1>}
Teraz zdeserializowane dane wejściowe da się wprost przekazać do fabryk lub konstruktora modeli, np:
instance = MyModel(**s.deserialize(...))
W ten sposób można całkowicie pominąć formularze django.