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 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') 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): 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`.