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('')
    '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', '')
    '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.

    >>> def find_recipe_in_joy(dish_url):
    ...     """Look up a dish in 'The Joy of Cooking'."""
    ...     return webservice.get("%s?ws.op=find_recipe_for&dish=%s" %
    ...         (joy_url, quote(dish_url))).jsonBody()

    >>> dish_url = webservice.get("/recipes/2").jsonBody()['dish_link']
    >>> find_recipe_in_joy(dish_url)['instructions']
    u'Draw, singe, stuff, and truss...'

The URL passed in to a named operation may be an absolute URL, or it
may be relative to the versioned service root. This is for developer
convenience only, as lazr.restful never serves relative URLs.

    >>> print dish_url
    http://cookbooks.dev/devel/dishes/Roast%20chicken
    >>> relative_url = quote("/dishes/Roast chicken")
    >>> find_recipe_in_joy(relative_url)['instructions']
    u'Draw, singe, stuff, and truss...'

A URL relative to the unversioned service root will not work.

    >>> relative_url = quote("/devel/dishes/Roast chicken")
    >>> find_recipe_in_joy(relative_url)
    Traceback (most recent call last):
    ...
    ValueError: dish: No such object "/devel/dishes/Roast%20chicken".


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_entry(url, representation, method, headers=None):
    ...     "A helper function to PUT or PATCH an entry."
    ...     new_headers = {'Content-type': 'application/json'}
    ...     if headers is not None:
    ...         new_headers.update(headers)
    ...     return webservice(
    ...         url, method, simplejson.dumps(representation), headers)

    >>> def modify_cookbook(cookbook, representation, method, headers=None):
    ...     "A helper function to PUT or PATCH a cookbook."
    ...     return modify_entry(
    ...         '/cookbooks/' + quote(cookbook), representation,
    ...         method, 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 may cause one of en entry's links to point to another
object. Here, we change the 'dish_link' field of a roast chicken
recipe, turning it into a recipe for baked beans.

    >>> old_dish = webservice.get("/recipes/1").jsonBody()['dish_link']
    >>> print old_dish
    http://.../dishes/Roast%20chicken

    >>> new_dish = webservice.get("/recipes/4").jsonBody()['dish_link']
    >>> print new_dish
    http://.../dishes/Baked%20beans

    >>> new_entry = modify_entry(
    ...     "/recipes/2", {'dish_link' : new_dish}, 'PATCH').jsonBody()
    >>> print new_entry['dish_link']
    http://.../dishes/Baked%20beans

When changing one of an entry's links, you can use an absolute URL (as
seen above) or a URL relative to the versioned service root. Let's use
a relative URL to change the baked beans recipe back to a roast
chicken recipe.

    >>> relative_old_dish = quote('/dishes/Roast chicken')
    >>> new_entry = modify_entry(
    ...     "/recipes/2", {'dish_link' : relative_old_dish},
    ...     'PATCH').jsonBody()
    >>> print new_entry['dish_link']
    http://.../dishes/Roast%20chicken

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
    ...

Here, the use of a nonexistent HTTP method causes an error.

    >>> print modify_cookbook('Everyday Greens',
    ...     {'cuisine' : 'General'}, 'POST',
    ...     {'X-HTTP-Method-Override' : 'NOSUCHMETHOD'})
    HTTP/1.1 405 Method Not Allowed
    ...

X-HTTP-Method-Override is only respected when the underlying HTTP
method is POST. If you use X-HTTP-Method-Override with any other HTTP
method, your value is ignored. Here, a nonexistent HTTP method is
ignored in favor of HTTP GET.

    >>> print webservice('/cookbooks/Everyday%20Greens', 'GET',
    ...     headers={'X-HTTP-Method-Override' : 'NOSUCHMETHOD'})
    HTTP/1.1 200 Ok
    ...
    Content-Type: application/json
    ...

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
    ...

Conditional writes are a little more complicated
************************************************

OK, but consider the 'copyright_date' field of a cookbook. This is
published as a read-only field; the client can't change it. But it's
not read-only on the server side. What if this value changes on the
server-side? What happens to the ETag then? Does it change, causing a
conditional PATCH to fail, even if the PATCH doesn't touch that
read-only field?

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> print greens['copyright_date']
    2003-01-01
    >>> etag_before_server_modification = greens['http_etag']

    >>> from lazr.restful.example.base.root import C4
    >>> greens_object = C4
    >>> old_date = greens_object.copyright_date
    >>> old_date
    datetime.date(2003, 1, 1)

Let's change the server-side value and find out.

    >>> import datetime
    >>> greens_object.copyright_date = datetime.date(2005, 12, 12)

    >>> new_greens = webservice.get(greens_url).jsonBody()
    >>> print new_greens['copyright_date']
    2005-12-12
    >>> etag_after_server_modification = new_greens['http_etag']

The ETag has indeed changed.

    >>> etag_before_server_modification == etag_after_server_modification
    False

So if we try to modify the cookbook using the old ETag, it should
fail, right?

    >>> body = {'description' : 'New description.'}
    >>> print modify_cookbook('Everyday Greens', body, 'PATCH',
    ...                       {'If-Match' : etag_before_server_modification})
    HTTP/1.1 209 Content Returned
    ...

Actually, it succeeds! How does that work? Well, the ETag consists of
two parts, separated by a dash.

    >>> read_before, write_before = etag_before_server_modification.split('-')
    >>> read_after, write_after = etag_after_server_modification.split('-')

The first part of the ETag only changes when a field the client can't
modify changes. This is the part of the ETag that changed when
copyright_date changed.

    >>> read_before == read_after
    False

The second part only changes when a field changes that the client can
modify. This part of the ETag hasn't changed.

    >>> write_before == write_after
    True

When you make a conditional write, the second part of the ETag you
provide is checked against the second part of the ETag generated on
the server. The first part of the ETag is ignored.

The point of checking the ETag is to avoid conflicts where two clients
modify the same resource. But no client can modify any of those
read-only fields, so changes to them don't matter for purposes of
avoiding conflicts.

If you hack the ETag to something that's not "two parts, separated by
a dash", lazr.restful will still handle it. (Of course, since that
ETag will never match anything, you'll always get a 412 error.)

    >>> body = {'description' : 'New description.'}
    >>> print modify_cookbook('Everyday Greens', body, 'PATCH',
    ...                       {'If-Match' : "Weird etag"})
    HTTP/1.1 412 Precondition Failed
    ...

Conditional PUT fails where a PATCH would succeed, but not because
lazr.restful rejects an old ETag. To verify this, let's change the
cookbook's copyright_date again, behind the scenes.

    >>> greens = webservice.get(greens_url).jsonBody()
    >>> old_etag = greens['http_etag']
    >>> greens_object.copyright_date = datetime.date(2005, 11, 11)

A totally bogus ETag fails with a 412 error.

    >>> greens['description'] = 'Another new description'
    >>> print modify_cookbook('Everyday Greens', greens, 'PUT',
    ...                       {'If-Match' : "Not the old ETag"})
    HTTP/1.1 412 Precondition Failed
    ...

When we use the original ETag, we don't cause a 412 error, but the PUT
request fails anyway.

    >>> print modify_cookbook('Everyday Greens', greens, 'PUT',
    ...                       {'If-Match' : old_etag})
    HTTP/1.1 400 Bad Request
    ...
    http_etag: You tried to modify a read-only attribute.
    copyright_date: You tried to modify a read-only attribute.

Rather, it's because a PUT request includes a representation of the
entire resource, and lazr.restful thinks the client is trying to
modify the fields that changed behind the scenes--in this case,
copyright_date and http_etag.

Conditional reads are *not* more complicated
********************************************

The point of the two-part ETag is to avoid spurious 412 errors when
doing conditional writes. When making a conditional _read_ request,
the condition will fail if _any_ part of the ETag is different.

    >>> new_etag = webservice.get(greens_url).jsonBody()['http_etag']

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

    >>> print webservice.get(
    ...     greens_url,
    ...     headers={'If-None-Match': "changed" + new_etag})
    HTTP/1.1 200 Ok
    ...

lazr.restful checks the entire ETag on conditional GET because the
purpose of a conditional read is to avoid getting data that hasn't
changed. A server-side change to a read-only field like copyright_date
doesn't affect future client writes, but it _is_ a change to the
representation.

A bit of cleanup: restore the old value for the cookbook's
copyright_date.

    >>> greens_object.copyright_date = old_date

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 = webservice.get(greens_url).jsonBody()
    >>> 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
    ...
