# 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