================
REST API helpers
================

There are a number of helpers that make building out the REST API easier.


Etags
=====

HTTP *etags* are a way for clients to decide whether their copy of a resource
has changed or not.  Mailman's REST API calculates this in a cheap and dirty
way.  Pass in the dictionary representing the resource and that dictionary
gets modified to contain the etag under the ``http_etag`` key.

    >>> from mailman.rest.helpers import etag
    >>> resource = dict(geddy='bass', alex='guitar', neil='drums')
    >>> json_data = etag(resource)
    >>> print(resource['http_etag'])
    "6929ecfbda2282980a4818fb75f82e812077f77a"

For convenience, the etag function also returns the JSON representation of the
dictionary after tagging, since that's almost always what you want.
::

    >>> import json
    >>> data = json.loads(json_data)

    # This is pretty close to what we want, so it's convenient to use.
    >>> dump_msgdata(data)
    alex     : guitar
    geddy    : bass
    http_etag: "6929ecfbda2282980a4818fb75f82e812077f77a"
    neil     : drums


POST and PUT unpacking
======================

Another helper unpacks ``POST`` and ``PUT`` request variables, validating and
converting their values.
::

    >>> from mailman.rest.validator import Validator
    >>> validator = Validator(one=int, two=str, three=bool)

    >>> class FakeRequest:
    ...     params = None
    ...     content_type = 'application/x-www-form-urlencoded'
    >>> FakeRequest.params = dict(one='1', two='two', three='yes')

On valid input, the validator can be used as a ``**keyword`` argument.

    >>> def print_request(one, two, three):
    ...     print(repr(one), repr(two), repr(three))
    >>> print_request(**validator(FakeRequest))
    1 'two' True

On invalid input, an exception is raised.

    >>> FakeRequest.params['one'] = 'hello'
    >>> print_request(**validator(FakeRequest))
    Traceback (most recent call last):
    ...
    ValueError: Invalid Parameter "one": invalid literal for int() with base 10: 'hello'.

On missing input, an exception is raised.

    >>> del FakeRequest.params['one']
    >>> print_request(**validator(FakeRequest))
    Traceback (most recent call last):
    ...
    ValueError: Missing Parameter: one

If more than one key is missing, it will be reflected in the error message.

    >>> del FakeRequest.params['two']
    >>> print_request(**validator(FakeRequest))
    Traceback (most recent call last):
    ...
    ValueError: Missing Parameter: one, two

Extra keys are also not allowed.

    >>> FakeRequest.params = dict(one='1', two='two', three='yes',
    ...                           four='', five='')
    >>> print_request(**validator(FakeRequest))
    Traceback (most recent call last):
    ...
    ValueError: Unexpected parameters: five, four

However, if optional keys are missing, it's okay.
::

    >>> validator = Validator(one=int, two=str, three=bool,
    ...                       four=int, five=int,
    ...                       _optional=('four', 'five'))

    >>> FakeRequest.params = dict(one='1', two='two', three='yes',
    ...                           four='4', five='5')
    >>> def print_request(one, two, three, four=None, five=None):
    ...     print(repr(one), repr(two), repr(three), repr(four), repr(five))
    >>> print_request(**validator(FakeRequest))
    1 'two' True 4 5

    >>> del FakeRequest.params['four']
    >>> print_request(**validator(FakeRequest))
    1 'two' True None 5

    >>> del FakeRequest.params['five']
    >>> print_request(**validator(FakeRequest))
    1 'two' True None None

But if the optional values are present, they must of course also be valid.

    >>> FakeRequest.params = dict(one='1', two='two', three='yes',
    ...                           four='no', five='maybe')
    >>> print_request(**validator(FakeRequest))
    Traceback (most recent call last):
    ...
    ValueError: Invalid Parameter "five": invalid literal for int() with base 10: 'maybe'. Invalid Parameter "four": invalid literal for int() with base 10: 'no'.


Arrays
======

Some ``POST`` forms include more than one value for a particular key.  This is
how lists and arrays are modeled.  The validator does the right thing with
such form data.  Specifically, when a key shows up multiple times in the form
data, a list is given to the validator.
::

    # We can't use a normal dictionary because we'll have multiple keys, but
    # the validator only wants to call .items() on the object.
    >>> class MultiDict:
    ...     def __init__(self, *params): self.values = list(params)
    ...     def items(self): return iter(self.values)
    >>> form_data = MultiDict(
    ...     ('one', '1'),
    ...     ('many', '3'),
    ...     ('many', '4'),
    ...     ('many', '5'),
    ...     )

This is a validation function that ensures the value is a list.

    >>> def must_be_list(value):
    ...     if not isinstance(value, list):
    ...         raise ValueError('not a list')
    ...     return [int(item) for item in value]

This is a validation function that ensure the value is *not* a list.

    >>> def must_be_scalar(value):
    ...     if isinstance(value, list):
    ...         raise ValueError('is a list')
    ...     return int(value)

And a validator to pull it all together.

    >>> validator = Validator(one=must_be_scalar, many=must_be_list)
    >>> FakeRequest.params = form_data
    >>> values = validator(FakeRequest)
    >>> print(values['one'])
    1
    >>> print(values['many'])
    [3, 4, 5]

The list values are guaranteed to be in the same order they show up in the
form data.

    >>> FakeRequest.params = MultiDict(
    ...     ('one', '1'),
    ...     ('many', '3'),
    ...     ('many', '5'),
    ...     ('many', '4'),
    ...     )
    >>> values = validator(FakeRequest)
    >>> print(values['one'])
    1
    >>> print(values['many'])
    [3, 5, 4]



PATCH Unpacking
===============

``PATCH`` requests are different from ``PUT`` and ``POST`` because with the
latter, you're changing the entire resource, so all expected attributes must
exist. With the former, you're only changing a subset of the attributes, so
you only validate the ones that exist in the request.
::

    >>> from mailman.rest.validator import PatchValidator
    >>> from mailman.rest.helpers import GetterSetter
    >>> values = dict(one=GetterSetter(int),
    ...               two=GetterSetter(str),
    ...               three=GetterSetter(bool))
    >>> FakeRequest.params = dict(one=1)
    >>> validator = PatchValidator(FakeRequest, values)

``PatchValidator`` can be used to update the attributes of an object directly:


    >>> class FakeObject:
    ...     one = 2
    >>> fakeobj = FakeObject()
    >>> validator.update(fakeobj, FakeRequest)
    >>> print(fakeobj.one)
    1



JSON Unpacking
==============

Request can optionally consist of JSON body as parameters. If the
``Content-Type`` header is set to ``application/json``, request's body is parsed
to set ``request.media`` as a dict object
::

    >>> validator = Validator(one=int, two=str, three=bool)

    >>> class FakeRequest:
    ...     params = None
    ...     content_type = 'application/json'
    ...     media = None
    >>> FakeRequest.media = dict(one='1', two='two', three='yes')

On valid input, the validator can be used as a ``**keyword`` argument.

    >>> def print_request(one, two, three):
    ...     print(repr(one), repr(two), repr(three))
    >>> print_request(**validator(FakeRequest))
    1 'two' True
