:mod:`formalchemy.validators` -- Validation stuff
=================================================

.. Commented imports

  >>> from formalchemy.tests import *

.. automodule:: formalchemy.validators

To validate data, you must bind it to your :class:`~formalchemy.forms.FieldSet`
along with the SQLAlchemy model.  Normally, you will retrieve `data` from a
dict::

  >>> from formalchemy.tests import User, bill
  >>> from formalchemy.forms import FieldSet
  >>> fs = FieldSet(User)
  >>> fs.configure(include=[fs.name]) # we only use the name field here
  >>> fs.rebind(bill, data={'User-1-name': 'Sam'})

Validation is performed simply by invoking `fs.validate()`, which
returns True if validation was successful, and False otherwise.
Validation functions are added with the `validate` method, described above.

If validation fails, `fs.errors` will be populated.  `errors` is a
dictionary of validation failures, and is always empty before `validate()`
is run.  Dictionary keys are attributes; values are lists of messages
given to `ValidationException`.  Global errors (not specific to a
single attribute) are under the key `None`.

Rendering a :class:`~formalchemy.forms.FieldSet` with errors will result in
error messages being displayed inline.  Here's what this looks like for a
required field that was not supplied with a value:

.. sourcecode:: html

    Foo

    Please enter a value

If validation succeeds, you can have `FormAlchemy` put the submitted
data back into the bound model object with `fs.sync`.  (If you bound to
a class instead of an object, the class will be instantiated for you.)
The object will be placed into the current session, if one exists::

  >>> if fs.validate(): fs.sync()
  >>> print bill.name
  Sam


Exception
---------

All validators raise a `ValidationError` if the validation failed.

.. exception:: ValidationError

Validators
----------

`formalchemy.validators` contains two types of functions: validation functions
that can be used directly, and validation function _generators_ that _return_ a
validation function satisfying some conditon.  E.g., `validators.maxlength(30)`
will return a validation function that can then be passed to `validate`.

  >>> from formalchemy.validators import *

Validation Functions
********************

A validation function is simply a function that, given a value, raises 
a ValidationError if it is invalid.

..
  >>> field = fs.name

.. autofunction:: integer

  >>> integer('1', field)
  1
  >>> integer('1.2', field)
  Traceback (most recent call last):
  ...
  ValidationError: Value is not an integer

.. autofunction:: float_

  >>> float_('1', field)
  1.0
  >>> float_('1.2', field)
  1.2
  >>> float_('asdf', field)
  Traceback (most recent call last):
  ...
  ValidationError: Value is not a number

.. autofunction:: currency

  >>> currency('asdf', field)
  Traceback (most recent call last):
  ...
  ValidationError: Value is not a number
  >>> currency('1', field)
  Traceback (most recent call last):
  ...
  ValidationError: Please specify full currency value, including cents (e.g., 12.34)
  >>> currency('1.0', field)
  Traceback (most recent call last):
  ...
  ValidationError: Please specify full currency value, including cents (e.g., 12.34)
  >>> currency('1.00', field)

.. autofunction:: required

  >>> required('asdf', field)
  >>> required('', field)
  Traceback (most recent call last):
  ...
  ValidationError: Please enter a value

.. autofunction:: email

  >>> email('a+formalchemy@gmail.com', field)
  >>> email('a+."<>"@gmail.com', field)
  >>> email('a+."<>"."[]"@gmail.com', field)
  >>> email('a+."<>@gmail.com', field)
  Traceback (most recent call last):
  ...
  ValidationError: Unterminated quoted section in recipient
  >>> email('a+."<>""[]"@gmail.com', field)
  Traceback (most recent call last):
  ...
  ValidationError: Quoted section must be followed by '@' or '.'
  >>> email('<>@gmail.com', field)
  Traceback (most recent call last):
  ...
  ValidationError: Reserved character present in recipient
  >>> email(chr(0) + '@gmail.com', field)
  Traceback (most recent call last):
  ...
  ValidationError: Control characters present
  >>> email(chr(129) + '@gmail.com', field)
  Traceback (most recent call last):
  ...
  ValidationError: Non-ASCII characters present
  >>> email('', field)
  >>> email('asdf', field)
  Traceback (most recent call last):
  ...
  ValidationError: Missing @ sign
  >>> email('@', field)
  Traceback (most recent call last):
  ...
  ValidationError: Recipient must be non-empty
  >>> email('a@', field)
  Traceback (most recent call last):
  ...
  ValidationError: Domain must be non-empty
  >>> email('a@gmail.com.', field)
  Traceback (most recent call last):
  ...
  ValidationError: Domain must not end with '.'
  >>> email('a@gmail..com', field)
  Traceback (most recent call last):
  ...
  ValidationError: Domain must not contain '..'
  >>> email('a@gmail>com', field)
  Traceback (most recent call last):
  ...
  ValidationError: Reserved character present in domain

Function generators
*******************

.. autofunction:: length

  >>> length(min=2, max=5)('a', field)
  Traceback (most recent call last):
  ...
  ValidationError: Value must be at least 2 characters long
  >>> length(min=2, max=5)('abcdef', field)
  Traceback (most recent call last):
  ...
  ValidationError: Value must be no more than 5 characters long
  >>> length(min=2, max=5)('abcde', field)

.. autofunction:: minlength

  >>> minlength(0)('a', field)
  Traceback (most recent call last):
  ...
  ValueError: Invalid minimum length
  >>> minlength(2)('a', field)
  Traceback (most recent call last):
  ...
  ValidationError: Value must be at least 2 characters long
  >>> minlength(2)('ab', field)

.. autofunction:: maxlength

  >>> maxlength(0)('a', field)
  Traceback (most recent call last):
  ...
  ValueError: Invalid maximum length
  >>> maxlength(1)('a', field)
  >>> maxlength(1)('ab', field)
  Traceback (most recent call last):
  ...
  ValidationError: Value must be no more than 1 characters long

.. autofunction:: regex

  >>> regex('[A-Z]+$')('ASDF', field)
  >>> regex('[A-Z]+$')('abc', field)
  Traceback (most recent call last):
  ...
  ValidationError: Invalid input
  >>> import re
  >>> pattern = re.compile('[A-Z]+$', re.I)
  >>> regex(pattern)('abc')

Write your own validator
------------------------

You can write your own validator, with the following function signature. The
`field` parameter will be the `Field` object being validated (and though its
`.parent` attribute, the `FieldSet`::

  >>> def negative(value, field):
  ...     if not (isinstance(value, int) and value < 0):
  ...         raise ValidationError('Value must be less than 0')

Then bind it to a field::

  >>> from formalchemy import types
  >>> fs = FieldSet(One)
  >>> fs.append(Field('number', type=types.Integer))
  >>> fs.configure(include=[fs.number.validate(negative)])

Then it should work::  

  >>> fs.rebind(One, data={'One--number': '-2'})
  >>> fs.validate()
  True

  >>> fs.rebind(One, data={'One--number': '2'})
  >>> fs.validate()
  False

You can also use the `field` positional argument to compare with some other fields
in the same FieldSet if you know this will be contained in a FieldSet, for example:

  >>> def passwd2_validator(value, field):
  ...     if field.parent.passwd1.value != value:
  ...         raise validators.ValidationError('Password do not match')

The `FieldSet.errors` and `Field.errors` attributes contain your custom error
message::

  >>> fs.errors
  {AttributeField(number): ['Value must be less than 0']}

  >>> fs.number.errors
  ['Value must be less than 0']

