214 lines
7.8 KiB
ReStructuredText
214 lines
7.8 KiB
ReStructuredText
Extending Colander
|
|
==================
|
|
|
|
You can extend Colander by defining a new :term:`type` or by defining
|
|
a new :term:`validator`.
|
|
|
|
.. _defining_a_new_type:
|
|
|
|
Defining a New Type
|
|
-------------------
|
|
|
|
A new type is a class with three methods:: ``serialize``, ``deserialize``,
|
|
and ``cstruct_children``. ``serialize`` converts a Python data structure (an
|
|
:term:`appstruct`) into a serialization (a :term:`cstruct`). ``deserialize``
|
|
converts a serialized value (a :term:`cstruct`) into a Python data structure
|
|
(a :term:`appstruct`). ``cstruct_children`` picks apart a :term:`cstruct`
|
|
it's passed and attempts to returns its child values in a list, based on the
|
|
children defined in the node it's passed.
|
|
|
|
.. note::
|
|
|
|
The ``cstruct_children`` method became required in Colander 0.9.9.
|
|
|
|
An Example
|
|
~~~~~~~~~~
|
|
|
|
Here's a type which implements boolean serialization and deserialization. It
|
|
serializes a boolean to the string ``true`` or ``false`` or the special
|
|
:attr:`colander.null` sentinel; it then deserializes a string (presumably
|
|
``true`` or ``false``, but allows some wiggle room for ``t``, ``on``,
|
|
``yes``, ``y``, and ``1``) to a boolean value.
|
|
|
|
.. code-block:: python
|
|
:linenos:
|
|
|
|
from colander import Invalid
|
|
from colander import null
|
|
|
|
class Boolean(object):
|
|
def serialize(self, node, appstruct):
|
|
if appstruct is null:
|
|
return null
|
|
if not isinstance(appstruct, bool):
|
|
raise Invalid(node, '%r is not a boolean' % appstruct)
|
|
return appstruct and 'true' or 'false'
|
|
|
|
def deserialize(self, node, cstruct):
|
|
if cstruct is null:
|
|
return null
|
|
if not isinstance(cstruct, basestring):
|
|
raise Invalid(node, '%r is not a string' % cstruct)
|
|
value = cstruct.lower()
|
|
if value in ('true', 'yes', 'y', 'on', 't', '1'):
|
|
return True
|
|
return False
|
|
|
|
def cstruct_children(self, node, cstruct):
|
|
return []
|
|
|
|
Here's how you would use the resulting class as part of a schema:
|
|
|
|
.. code-block:: python
|
|
:linenos:
|
|
|
|
import colander
|
|
|
|
class Schema(colander.MappingSchema):
|
|
interested = colander.SchemaNode(Boolean())
|
|
|
|
The above schema has a member named ``interested`` which will now be
|
|
serialized and deserialized as a boolean, according to the logic defined in
|
|
the ``Boolean`` type class.
|
|
|
|
Implementing Type Classes
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The constraints of a type class implementation are:
|
|
|
|
- It must have both a ``serialize`` and ``deserialize`` method.
|
|
|
|
- it must deal specially with the value :attr:`colander.null` within both
|
|
``serialize`` and ``deserialize``.
|
|
|
|
- its ``serialize`` method must be able to make sense of a value generated by
|
|
its ``deserialize`` method and vice versa.
|
|
|
|
- its ``cstruct_children`` method must return an empty list if the node it's
|
|
passed has no children, or a value for each child node in the node it's
|
|
passed based on the ``cstruct``.
|
|
|
|
The ``serialize`` method of a type accepts two values: ``node``, and
|
|
``appstruct``. ``node`` will be the schema node associated with this type.
|
|
The node is used when the type must raise a :exc:`colander.Invalid` error,
|
|
which expects a schema node as its first constructor argument. ``appstruct``
|
|
will be the :term:`appstruct` value that needs to be serialized.
|
|
|
|
The deserialize and method of a type accept two values: ``node``, and
|
|
``cstruct``. ``node`` will be the schema node associated with this type.
|
|
The node is used when the type must raise a :exc:`colander.Invalid` error,
|
|
which expects a schema node as its first constructor argument. ``cstruct``
|
|
will be the :term:`cstruct` value that needs to be deserialized.
|
|
|
|
The ``cstruct_children`` method accepts two values: ``node`` and ``cstruct``.
|
|
``node`` will be the schema node associated with this type. ``cstruct`` will
|
|
be the :term:`cstruct` that the caller wants to obtain child values for. The
|
|
``cstruct_children`` method should *never* raise an exception, even if it
|
|
passed a nonsensical value. If it is passed a nonsensical value, it should
|
|
return a sequence of ``colander.null`` values; the sequence should contain as
|
|
many nulls as there are node children. If the ``cstruct`` passed does not
|
|
contain a value for a particular child, that child should be replaced with
|
|
the ``colander.null`` value in the returned list. Generally, if the type
|
|
you're defining is not expected to have children, it's fine to return an
|
|
empty list from ``cstruct_children``. It's only useful for complex types
|
|
such as mappings and sequences, usually.
|
|
|
|
Null Values
|
|
~~~~~~~~~~~
|
|
|
|
The framework requires that both the ``serialize`` method and the
|
|
``deserialize`` method of a type explicitly deal with the potential to
|
|
receive a :attr:`colander.null` value. :attr:`colander.null` will be sent to
|
|
the type during serialization and deserialization in circumstances where a
|
|
value has not been provided by the data structure being serialized or
|
|
deserialized. In the common case, when the ``serialize`` or ``deserialize``
|
|
method of type receives the :attr:`colander.null` value, it should just
|
|
return :attr:`colander.null` to its caller.
|
|
|
|
A type might also choose to return :attr:`colander.null` if the value it
|
|
receives is *logically* (but not literally) null. For example,
|
|
:class:`colander.String` type converts the empty string to ``colander.null``
|
|
within its ``deserialize`` method.
|
|
|
|
.. code-block:: python
|
|
:linenos:
|
|
|
|
def deserialize(self, node, cstruct):
|
|
if not cstruct:
|
|
return null
|
|
|
|
Type Constructors
|
|
~~~~~~~~~~~~~~~~~
|
|
|
|
A type class does not need to implement a constructor (``__init__``),
|
|
but it isn't prevented from doing so if it needs to accept arguments;
|
|
Colander itself doesn't construct any types, only users of Colander
|
|
schemas do, so how types are constructed is beyond the scope of
|
|
Colander itself.
|
|
|
|
The :exc:`colander.Invalid` exception may be raised during
|
|
serialization or deserialization as necessary for whatever reason the
|
|
type feels appropriate (the inability to serialize or deserialize a
|
|
value being the most common case).
|
|
|
|
For a more formal definition of a the interface of a type, see
|
|
:class:`colander.interfaces.Type`.
|
|
|
|
.. _defining_a_new_validator:
|
|
|
|
Defining a New Validator
|
|
------------------------
|
|
|
|
A validator is a callable which accepts two positional arguments:
|
|
``node`` and ``value``. It returns ``None`` if the value is valid.
|
|
It raises a :class:`colander.Invalid` exception if the value is not
|
|
valid. Here's a validator that checks if the value is a valid credit
|
|
card number.
|
|
|
|
.. code-block:: python
|
|
:linenos:
|
|
|
|
def luhnok(node, value):
|
|
""" checks to make sure that the value passes a luhn mod-10 checksum """
|
|
sum = 0
|
|
num_digits = len(value)
|
|
oddeven = num_digits & 1
|
|
|
|
for count in range(0, num_digits):
|
|
digit = int(value[count])
|
|
|
|
if not (( count & 1 ) ^ oddeven ):
|
|
digit = digit * 2
|
|
if digit > 9:
|
|
digit = digit - 9
|
|
|
|
sum = sum + digit
|
|
|
|
if not (sum % 10) == 0:
|
|
raise Invalid(node,
|
|
'%r is not a valid credit card number' % value)
|
|
|
|
Here's how the resulting ``luhnok`` validator might be used in a
|
|
schema:
|
|
|
|
.. code-block:: python
|
|
:linenos:
|
|
|
|
import colander
|
|
|
|
class Schema(colander.MappingSchema):
|
|
cc_number = colander.SchemaNode(colander.String(), validator=lunhnok)
|
|
|
|
Note that the validator doesn't need to check if the ``value`` is a
|
|
string: this has already been done as the result of the type of the
|
|
``cc_number`` schema node being :class:`colander.String`. Validators
|
|
are always passed the *deserialized* value when they are invoked.
|
|
|
|
The ``node`` value passed to the validator is a schema node object; it
|
|
must in turn be passed to the :exc:`colander.Invalid` exception
|
|
constructor if one needs to be raised.
|
|
|
|
For a more formal definition of a the interface of a validator, see
|
|
:class:`colander.interfaces.Validator`.
|
|
|