Entries
*******

Most objects published by a lazr.restful web service are entries:
self-contained data structures with an independent existence from any
other entry. Entries are distinguished from collections, which are
groupings of entries.

All entries in a web service work pretty much the same way. This
document illustrates the general features of entries, using the
example web service's dishes and recipes as examples.

    >>> from lazr.restful.testing.webservice import WebServiceCaller
    >>> webservice = WebServiceCaller(domain='cookbooks.dev')

=======
Reading
=======

It's possible to get a JSON 'representation' of an entry by sending a
GET request to the entry's URL.

Here we see that the cookbook 'Everyday Greens' is a vegetarian cookbook.

    >>> from urllib import quote
    >>> greens_url = quote("/cookbooks/Everyday Greens")
    >>> webservice.get(greens_url).jsonBody()['cuisine']
    u'Vegetarian'

Data is served encoded in UTF-8, and a good client will automatically
convert it into Unicode.

    >>> construsions_url = quote("/cookbooks/Construsions un repas")
    >>> webservice.get(construsions_url).jsonBody()['cuisine']
    u'Fran\xe7aise'

Content negotiation
===================

By varying the 'Accept' header, the client can request either a JSON
or XHTML representation of an entry, or a WADL description of the
entry's capabilities.

    >>> def negotiated_type(accept_header,
    ...                     uri='/cookbooks/Everyday%20Greens'):
    ...     return webservice.get(
    ...         uri, accept_header).getheader('Content-Type')

    >>> negotiated_type('application/json')
    'application/json'

    >>> negotiated_type('application/xhtml+xml')
    'application/xhtml+xml'

    >>> negotiated_type('application/vnd.sun.wadl+xml')
    'application/vnd.sun.wadl+xml'

    >>> negotiated_type(None)
    'application/json'

    >>> negotiated_type('text/html')
    'application/json'

    >>> negotiated_type('application/json, application/vnd.sun.wadl+xml')
    'application/json'

    >>> negotiated_type('application/json, application/xhtml+xml')
    'application/json'

    >>> negotiated_type('application/vnd.sun.wadl+xml, text/html, '
    ...                 'application/json')
    'application/vnd.sun.wadl+xml'

    >>> negotiated_type('application/json;q=0.5, application/vnd.sun.wadl+xml')
    'application/vnd.sun.wadl+xml'

    >>> negotiated_type('application/json;q=0, application/xhtml+xml;q=0.05,'
    ...                 'application/vd.sun.wadl+xml;q=0.1')
    'application/vd.sun.wadl+xml'

The client can also set the 'ws.accept' query string variable, which
will take precedence over any value set for the Accept header.

    >>> def qs_negotiated_type(query_string, header):
    ...     uri = '/cookbooks/Everyday%20Greens?ws.accept=' + query_string
    ...     return negotiated_type(header, uri)

    >>> qs_negotiated_type('application/json', None)
    'application/json'

    >>> qs_negotiated_type('application/json', 'application/xhtml+xml')
    'application/json'

    >>> negotiated_type('application/json;q=0, application/xhtml+xml;q=0.5,'
    ...                 'application/json;q=0.5, application/xhtml+xml;q=0,')
    'application/xhtml+xml'

Earlier versions of lazr.restful served a misspelling of the WADL
media type. For purposes of backwards compatibility, lazr.restful
will still serve this media type if it's requested.

    >>> negotiated_type('application/vd.sun.wadl+xml')
    'application/vd.sun.wadl+xml'

XHTML representations
=====================

Every entry has an XHTML representation. The default representation is
a simple definition list.

    >>> print webservice.get(greens_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <dl ...>
    ...
    </dl>

Getting the XHTML representation works correctly even when some of the fields
have non-ascii values.

    >>> print webservice.get(construsions_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <dl ...>
    ...
    <BLANKLINE>
     <dt>cuisine</dt>
     <dd>Française</dd>
    <BLANKLINE>
    ...
    </dl>

But it's possible to define a custom HTML view for a particular object
type. Here's a simple view that serves some hard-coded HTML.

    >>> class DummyView:
    ...
    ...     def __init__(*args):
    ...         pass
    ...
    ...     def __call__(*args):
    ...         return "<html>foo</html>"

Register the view as the IWebServiceClientRequest view for an
ICookbook entry...

    >>> from lazr.restful.interfaces import IWebServiceClientRequest
    >>> from lazr.restful.example.base.interfaces import ICookbook
    >>> from zope.interface.interfaces import IInterface
    >>> view_name = "lazr.restful.EntryResource"
    >>> from zope.component import getGlobalSiteManager
    >>> manager = getGlobalSiteManager()
    >>> manager.registerAdapter(
    ...      factory=DummyView,
    ...      required=[ICookbook, IWebServiceClientRequest],
    ...      provided=IInterface, name=view_name)

...and the XHTML representation of an ICookbook will be the result of
calling a DummyView object.

    >>> print webservice.get(greens_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <html>foo</html>

Before we continue, here's some cleanup code to remove the custom view
we just defined.

    >>> from zope.component import getGlobalSiteManager
    >>> ignored = getGlobalSiteManager().unregisterAdapter(
    ...      factory=DummyView,
    ...      required=[ICookbook, IWebServiceClientRequest],
    ...      provided=IInterface, name=view_name)

    >>> print webservice.get(greens_url, 'application/xhtml+xml')
    HTTP/1.1 200 Ok
    ...
    <dl ...>
    ...
    </dl>

Visibility
==========

There are two recipes in "James Beard's American Cookery", but one of
them has been marked private. The private one cannot be retrieved.

    >>> print webservice.get('/recipes/3')
    HTTP/1.1 200 Ok
    ...

    >>> print webservice.get('/recipes/5')
    HTTP/1.1 401 Unauthorized
    ...

If a resource itself is visible to the client, but it contains
information that's not visible, the information will be
redacted. Here, a cookbook resource is visible to our client, but its
value for the 'confirmed' field is not visible.

    >>> cookbook = webservice.get(greens_url).jsonBody()
    >>> print cookbook['name']
    Everyday Greens
    >>> print cookbook['confirmed']
    tag:launchpad.net:2008:redacted

Named operations
================

Some entries support custom operations through GET. The custom
operation to be invoked is named in the query string's 'ws.op'
argument. You can search a cookbook's recipes by specifying
the 'find_recipes' operation.

    >>> joy_url = quote("/cookbooks/The Joy of Cooking")
    >>> recipes = webservice.get(
    ...     "%s?ws.op=find_recipes&search=e" % joy_url).jsonBody()
    >>> sorted([r['self_link'] for r in recipes['entries']])
    [u'.../recipes/2', u'.../recipes/4']

A named operation can take as an argument the URL to another
object. Here the 'dish' argument is the URL to a dish, and the named
operation finds a recipe for making that dish.

    >>> dish_url = webservice.get("/recipes/2").jsonBody()['dish_link']
    >>> recipe = webservice.get("%s?ws.op=find_recipe_for&dish=%s" %
    ...                         (joy_url, quote(dish_url))).jsonBody()
    >>> recipe['instructions']
    u'Draw, singe, stuff, and truss...'

Some entries support custom operations through POST. You can invoke a
custom operation to modify a cookbook's name, making it seem more
interesting.

    >>> print webservice.get(joy_url).jsonBody()['cuisine']
    General

    >>> print webservice.named_post(joy_url, 'make_more_interesting', {})
    HTTP/1.1 200 Ok
    ...

    >>> new_joy_url = quote("/cookbooks/The New The Joy of Cooking")
    >>> print webservice.get(new_joy_url).jsonBody()['name']
    The New The Joy of Cooking

Custom operations may have error handling.

    >>> print webservice.named_post(new_joy_url, 'make_more_interesting', {})
    HTTP/1.1 400 Bad Request
    ...
    The 'New' trick can't be used on this cookbook because its
    name already starts with 'The New'.
    ...

    >>> import simplejson
    >>> ignore = webservice.patch(
    ...     new_joy_url, 'application/json',
    ...     simplejson.dumps({"name": "The Joy of Cooking"}))

Trying to invoke a nonexistent custom operation yields an error.

    >>> print webservice.get("%s?ws.op=no_such_operation" % joy_url)
    HTTP/1.1 400 Bad Request
    ...
    No such operation: no_such_operation

============
Modification
============

It's possible to modify an entry by sending to the server a document
asserting what the entry should look like. The document may only
describe part of the entry's new state, in which case the client
should use the PATCH HTTP method. Or it may completely describe the
entry's state, in which case the client should use PUT.

    >>> def modify_cookbook(cookbook, representation, method, headers=None):
    ...     "A helper function to PUT or PATCH a cookbook."
    ...     new_headers = {'Content-type': 'application/json'}
    ...     if headers is not None:
    ...         new_headers.update(headers)
    ...     return webservice('/cookbooks/' + quote(cookbook), method,
    ...                       simplejson.dumps(representation),
    ...                       headers)

Here we use the web service to change the cuisine of the "Everyday
Greens" cookbook. The data returned is the new JSON representation of
the object.

    >>> print webservice.get(greens_url).jsonBody()['revision_number']
    0

    >>> result = modify_cookbook('Everyday Greens', {'cuisine' : 'American'},
    ...                          'PATCH')
    >>> print result
    HTTP/1.1 209 Content Returned
    ...
    Content-Type: application/json
    ...
    {...}

    >>> greens = result.jsonBody()
    >>> print greens['cuisine']
    American

Whenever a client modifies a cookbook, the revision_number is
incremented behind the scenes.

    >>> print greens['revision_number']
    1

A modification might cause an entry's address to change. Here we use
the web service to change the cookbook's name to 'Everyday Greens 2'.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'name' : 'Everyday Greens 2'}, 'PATCH')
    HTTP/1.1 301 Moved Permanently
    ...
    Location: http://.../Everyday%20Greens%202
    ...

At this point we can no longer manipulate this cookbook by sending
HTTP requests to http://cookbooks.dev/1.0/cookbooks/Everyday%20Greens,
because that cookbook now 'lives' at
http://cookbooks.dev/1.0/cookbooks/Everyday%20Greens%202. To change
the cookbook name back, we need to send a PATCH request to the new
address.

    >>> print modify_cookbook('Everyday Greens 2',
    ...                       {'name' : 'Everyday Greens'}, 'PATCH')
    HTTP/1.1 301 Moved Permanently
    ...
    Location: http://.../cookbooks/Everyday%20Greens
    ...

The PATCH HTTP method is useful for simple changes, but not all HTTP
clients support PATCH. It's possible to fake a PATCH request with
POST, by setting the X-HTTP-Method-Override header to "PATCH". Because
Firefox 3 mangles the Content-Type header for POST requests, you may
also set the X-Content-Type-Override header, which will override the
value of Content-Type.

    >>> print modify_cookbook('Everyday Greens',
    ...     {'cuisine' : 'General'}, 'POST',
    ...     {'X-HTTP-Method-Override' : 'PATCH',
    ...      'Content-Type': 'not-a-valid-content/type',
    ...      'X-Content-Type-Override': 'application/json'})
    HTTP/1.1 209 Content Returned
    ...

If you try to use X-HTTP-Method-Override when the underlying HTTP
method is not POST, you'll get an error.

    >>> print modify_cookbook('Everyday Greens',
    ...     {}, 'GET', {'X-HTTP-Method-Override' : 'PATCH'})
    HTTP/1.1 400 Bad Request
    ...
    X-HTTP-Method-Override can only be used with a POST request.

Even if a client supports PATCH, sometimes it's easier to GET a
document, modify it, and send it back. If you have the full document
at hand, you can use the PUT method.

We happen to have a full document from when we sent a GET request to
the 'Everday Greens' cookbook. Modifying that document and PUTting it
back is less work than constructing a new document and sending it with
PATCH. As with PATCH, a successful PUT serve the new representation of
the object that was modified.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> print greens['cuisine']
    General

    >>> greens['cuisine'] = 'Vegetarian'
    >>> print modify_cookbook('Everyday Greens', greens, 'PUT')
    HTTP/1.1 209 Content Returned
    ...
    {...}

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> print greens['cuisine']
    Vegetarian

Because our patch format is the same as our representation format (a
JSON hash), any document that works with a PUT request will also work
with a PATCH request.

    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH')
    HTTP/1.1 209 Content Returned
    ...

Content negotiation during modification
=======================================

When making a PATCH, you don't have to get a JSON representation
back. You can also get an HTML representation.

    >>> print modify_cookbook('Everyday Greens', {}, 'PATCH',
    ...                       headers={'Accept': 'application/xhtml+xml'})
    HTTP/1.1 209 Content Returned
    ...
    Content-Type: application/xhtml+xml
    ...
    <?xml version="1.0"?>
    ...

You can even get a WADL representation, though that's pretty useless.

    >>> headers = {'Accept':'application/vd.sun.wadl+xml'}
    >>> print modify_cookbook('Everyday Greens', {}, 'PATCH',
    ...                       headers=headers)
    HTTP/1.1 209 Content Returned
    ...
    Content-Type: application/vd.sun.wadl+xml
    ...

Server-side modification
========================

Sometimes the server will transparently modify a value sent by the
client, to clean it up or put it into a canonical form. For this
purpose, the response to a PUT or PATCH request includes a brand new
JSON representation of the object, so that the client can know whether
and which changes were made.

Here's an example. If a cookbook's description contains leading or trailing
whitespace, the whitespace will be stripped.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> greens['description']
    u''
    >>> first_etag = greens['http_etag']

Send in a name with leading or trailing whitespace and it'll be
transparently trimmed. The document returned from the POST request
will be the new representation, modified by both client and server.

    >>> greens = webservice(
    ...     greens_url, "PATCH",
    ...     simplejson.dumps({'description' : '  A description '}),
    ...     {'Content-type': 'application/json'}).jsonBody()
    >>> greens['description']
    u'A description'
    >>> greens['http_etag'] == first_etag
    False

The canonicalization works for PUT requests as well.

    >>> greens['description'] = "    Another description  "
    >>> greens = webservice(greens_url, "PUT", simplejson.dumps(greens),
    ...                     {'Content-type': 'application/json'}).jsonBody()
    >>> greens['description']
    u'Another description'

Conditional GET, PUT and PATCH
==============================

When you GET an entry you're given an ETag; an opaque string that
changes whenever the entry changes.

    >>> response = webservice.get(greens_url)
    >>> greens_etag = response.getheader('ETag')
    >>> greens = response.jsonBody()

The ETag is present in the HTTP response headers when you GET an
entry, but it's also present in the representation of the entry
itself.

    >>> greens['http_etag'] == greens_etag
    True

This is so you can get the ETags for all the entries in a collection
at once, without making a separate HTTP request for each.

    >>> cookbooks = webservice.get('/cookbooks').jsonBody()
    >>> etags = [book['http_etag'] for book in cookbooks['entries']]

The ETag provided with an entry of a collection is the same as the
ETag you'd get if you got that entry individually.

    >>> first_book = cookbooks['entries'][0]
    >>> first_book_2 = webservice.get(first_book['self_link']).jsonBody()
    >>> first_book['http_etag'] == first_book_2['http_etag']
    True

When you make a GET request, you can provide the ETag as the
If-None-Match header. This lets you save time when the resource hasn't
changed.

    >>> print webservice.get(greens_url,
    ...                      headers={'If-None-Match': greens_etag})
    HTTP/1.1 304 Not Modified
    ...

Conditional GET works the same way whether the request goes through
the web service's virtual host or through the website-level interface
designed for Ajax.

    >>> from lazr.restful.testing.webservice import WebServiceAjaxCaller
    >>> ajax = WebServiceAjaxCaller(domain='cookbooks.dev')
    >>> etag = 'dummy-etag'
    >>> response = ajax.get(greens_url, headers={'If-None-Match' : etag})
    >>> etag = response.getheader("Etag")
    >>> print ajax.get(greens_url, headers={'If-None-Match' : etag})
    HTTP/1.1 304 Not Modified
    ...

When you make a PUT or PATCH request, you can provide the ETag as the
If-Match header. This lets you detect changes that other people made
to the entry, so your changes don't overwrite theirs.

If the ETag you provide in If-Match matches the entry's current ETag,
your request goes through.

    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
    ...                     {'If-Match' : greens_etag})
    HTTP/1.1 209 Content Returned
    ...

If the ETags don't match, it's because somebody modified the entry
after you got your copy of it. Your request will fail with status code
412.

    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
    ...                       {'If-Match' : '"an-old-etag"'})
    HTTP/1.1 412 Precondition Failed
    ...

If you specify a number of ETags, and any of them match, your request
will go through.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> match = '"an-old-etag", %s' % greens['http_etag']
    >>> print modify_cookbook('Everyday Greens', greens, 'PATCH',
    ...                       {'If-Match' : match})
    HTTP/1.1 209 Content Returned
    ...

Both PUT and PATCH requests work this way.

    >>> print modify_cookbook('Everyday Greens', greens, 'PUT',
    ...                       {'If-Match' : 'an-old-etag'})
    HTTP/1.1 412 Precondition Failed
    ...

Changing object relationships
=============================

In addition to changing an object's data fields, you can change its
relationships to other objects. Here we change which dish a recipe is
for.

    >>> recipe_url = '/recipes/3'
    >>> recipe = webservice.get(recipe_url).jsonBody()
    >>> print recipe['dish_link']
    http://.../dishes/Roast%20chicken

    >>> def modify_dish(url, recipe, new_dish_url):
    ...     recipe['dish_link'] = new_dish_url
    ...     return webservice.put(
    ...         url, 'application/json', simplejson.dumps(recipe))

    >>> new_dish = webservice.get(quote('/dishes/Baked beans')).jsonBody()
    >>> new_dish_url = new_dish['self_link']
    >>> recipe['dish_link'] = new_dish_url
    >>> print modify_dish(recipe_url, recipe, new_dish_url)
    HTTP/1.1 209 Content Returned
    ...

    >>> recipe = webservice.get(recipe_url).jsonBody()
    >>> print recipe['dish_link']
    http://.../dishes/Baked%20beans

Identification of the dish is done by specifying a URL; a random
string won't work.

    >>> print modify_dish(recipe_url, recipe, 'A random string')
    HTTP/1.1 400 Bad Request
    ...
    dish_link: "A random string" is not a valid URI.

But not just any URL will do. It has to identify an object in the web
service.

    >>> print modify_dish(recipe_url, recipe, 'http://www.canonical.com')
    HTTP/1.1 400 Bad Request
    ...
    dish_link: No such object "http://www.canonical.com".

    >>> print modify_dish(
    ...     recipe_url, recipe,
    ...     'http://www.canonical.com/dishes/Baked%20beans')
    HTTP/1.1 400 Bad Request
    ...
    dish_link: No such object "http://www.canonical.com/dishes/Baked%20beans".

This URL would be valid, but it uses the wrong protocol (HTTPS instead
of HTTP).

    >>> https_link = recipe['dish_link'].replace('http:', 'https:')
    >>> print modify_dish(recipe_url, recipe, https_link)
    HTTP/1.1 400 Bad Request
    ...
    dish_link: No such object "https://.../Baked%20beans".

Even a URL that identifies an object in the web service won't work, if
the object isn't the right kind of object. A recipe must be for a
dish, not a cookbook:

    >>> print modify_dish(recipe_url, recipe, recipe['cookbook_link'])
    HTTP/1.1 400 Bad Request
    ...
    dish_link: Your value points to the wrong kind of object

Date formats
============

lazr.restful web services serve and parse dates in ISO 8601
format. Only UTC dates are allowed.

The tests that follow make a number of PATCH requests that include
values for a cookbook's 'copyright_date' attribute.

    >>> greens['copyright_date']
    u'2003-01-01'

    >>> def patch_greens_copyright_date(date):
    ...     "A helper method to try and change a date field."
    ...     return modify_cookbook(
    ...         'Everyday Greens', {'copyright_date' : date}, 'PATCH')

These requests aren't actually trying to modify 'copyright_date', which
is read-only. They're asserting that 'copyright_date' is a certain
value. If the assertion succeeds (because 'copyright_date' does in fact
have that value), the response code is 200. If the assertion could not
be understood (because the date is in the wrong format), the response
code is 400, and the body is an error message about the date
format. If the assertion _fails_ (because 'copyright_date' happens to be
read-only), the response code is also 400, but the error message talks
about an attempt to modify a read-only attribute.

The two 400 error codes below are caused by a failure to understand
the assertion. The string used in the assertion might not be a date.

    >>> print patch_greens_copyright_date('dummy')
    HTTP/1.1 400 Bad Request
    ...
    copyright_date: Value doesn't look like a date.

Or it might be a date that's not in UTC.

    >>> print patch_greens_copyright_date(u'2005-06-06T00:00:00.000000+05:00')
    HTTP/1.1 400 Bad Request
    ...
    copyright_date: Time not in UTC.

There are five ways to specify UTC:

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000Z')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000+00:00')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000+0000')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000-00:00')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000-0000')
    HTTP/1.1 209 Content Returned
    ...

A value with a missing timezone is treated as UTC.

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00.000000')
    HTTP/1.1 209 Content Returned
    ...

Less precise time measurements may also be acceptable.

    >>> print patch_greens_copyright_date(u'2003-01-01T00:00:00Z')
    HTTP/1.1 209 Content Returned
    ...

    >>> print patch_greens_copyright_date(u'2003-01-01')
    HTTP/1.1 209 Content Returned
    ...

What you can't do
=================

A document that would be acceptable as the payload of a PATCH request
might not be acceptable as the payload of a PUT request.

    >>> print modify_cookbook('Everyday Greens', {'name' : 'Greens'}, 'PUT')
    HTTP/1.1 400 Bad Request
    ...
    You didn't specify a value for the attribute 'cuisine'.

A document that's not a valid JSON document is also unacceptable.

    >>> print webservice.patch(greens_url, "application/json", "{")
    HTTP/1.1 400 Bad Request
    ...
    Entity-body was not a well-formed JSON document.

A document that's valid JSON but is not a JSON hash is unacceptable.

    >>> print modify_cookbook('Everyday Greens', 'name=Greens', 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    Expected a JSON hash.

An entry's read-only attributes can't be modified.

    >>> print modify_cookbook(
    ...     'Everyday Greens',
    ...     {'copyright_date' : u'2001-01-01T01:01:01+00:00Z'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    copyright_date: You tried to modify a read-only attribute.

You can send a document that includes a value for a read-only
attribute, but it has to be the same as the current value.

    >>> print modify_cookbook(
    ...     'Everyday Greens',
    ...     {'copyright_date' : greens['copyright_date']}, 'PATCH')
    HTTP/1.1 209 Content Returned
    ...

You can't change the link to an entry's associated collection.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'recipes_collection_link' : 'dummy'},
    ...                       'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    recipes_collection_link: You tried to modify a collection...

Again, you can send a document that includes a value for an associated
collection link; you just can't _change_ the value.

    >>> print modify_cookbook(
    ...     'Everyday Greens',
    ...     {'recipes_collection_link' : greens['recipes_collection_link']},
    ...     'PATCH')
    HTTP/1.1 209 Content Returned
    ...

You can't directly change an entry's URL address.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'self_link' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    self_link: You tried to modify a read-only attribute.

You can't directly change an entry's ETag.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'http_etag' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    http_etag: You tried to modify a read-only attribute.

You can't change an entry's resource type.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'resource_type_link' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    resource_type_link: You tried to modify a read-only attribute.

You can't refer to a link to an associated object or collection as
though it were the actual object. A cookbook has a
'recipes_collection_link', but it doesn't have 'recipes' directly.

    >>> print modify_cookbook(
    ...     'Everyday Greens', {'recipes' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    recipes: You tried to modify a nonexistent attribute.

A recipe has a 'dish_link', but it doesn't have a 'dish' directly.

    >>> url = quote('/cookbooks/The Joy of Cooking/Roast chicken')
    >>> print webservice.patch(url, 'application/json',
    ...     simplejson.dumps({'dish' : 'dummy'}))
    HTTP/1.1 400 Bad Request
    ...
    dish: You tried to modify a nonexistent attribute.

You can't set values that violate data integrity rules. For instance,
you can't set a required value to None.

    >>> print modify_cookbook('Everyday Greens',
    ...                       {'name' : None}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    name: Missing required value.

And of course you can't modify attributes that don't exist.

    >>> print modify_cookbook(
    ...     'Everyday Greens', {'nonesuch' : 'dummy'}, 'PATCH')
    HTTP/1.1 400 Bad Request
    ...
    nonesuch: You tried to modify a nonexistent attribute.

Deletion
========

Some entries may be deleted with the HTTP DELETE method. In the
example web service, recipes can be deleted.

    >>> recipe_url = "/recipes/6"
    >>> print webservice.get(recipe_url)
    HTTP/1.1 200 Ok
    ...

    >>> print webservice.delete(recipe_url)
    HTTP/1.1 200 Ok
    ...

    >>> print webservice.get(recipe_url)
    HTTP/1.1 404 Not Found
    ...
