********************
RESTful Web Services
********************

lazr.restful builds on Zope conventions to make it easy to expose your model
objects as RESTful HTTP resources. I'll demonstrate these features by defining
a model for managing recipes, and then publishing the model objects as
resources through a web service.

Example model objects
=====================

Here's the interface for a simple set of model objects. This is the
kind of model object you'd find in any Zope application, with no
special knowledge of web services. The model is of a group of
cookbooks. Each cookbook has a known person as the author. Each
cookbook contains multiple recipes. A recipe is a recipe _for_ a dish,
and two or more cookbooks may provide different recipes for the same
dish. Users may comment on cookbooks and on individual recipes.

    # All classes defined in this test are new-style classes.
    >>> __metaclass__ = type

    >>> from zope.interface import Interface, Attribute
    >>> from zope.schema import Bool, Bytes, Int, Text, TextLine
    >>> from lazr.restful.fields import Reference

    >>> class ITestDataObject(Interface):
    ...     """A marker interface for data objects."""
    ...     path = Attribute("The path portion of this object's URL. "
    ...                      "Defined here for simplicity of testing.")

    >>> class IAuthor(ITestDataObject):
    ...     name = TextLine(title=u"Name", required=True)
    ...     # favorite_recipe.schema will be set to IRecipe once
    ...     # IRecipe is defined.
    ...     favorite_recipe = Reference(schema=Interface)
    ...     popularity = Int(readonly=True)

    >>> class ICommentTarget(ITestDataObject):
    ...     comments = Attribute('List of comments about this object.')

    >>> class IComment(ITestDataObject):
    ...     target = Attribute('The object containing this comment.')
    ...     text = TextLine(title=u"Text", required=True)

    >>> class ICookbook(ICommentTarget):
    ...     name = TextLine(title=u"Name", required=True)
    ...     author = Reference(schema=IAuthor)
    ...     cuisine = TextLine(title=u"Cuisine", required=False, default=None)
    ...     recipes = Attribute("List of recipes published in this cookbook.")
    ...     cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.")
    ...     def removeRecipe(recipe):
    ...         """Remove a recipe from this cookbook."""

    >>> class IDish(ITestDataObject):
    ...     name = TextLine(title=u"Name", required=True)
    ...     recipes = Attribute("List of recipes for this dish.")
    ...     def removeRecipe(recipe):
    ...         """Remove one of the recipes for this dish."""

    >>> class IRecipe(ICommentTarget):
    ...     id = Int(title=u"Unique ID", required=True)
    ...     dish = Reference(schema=IDish)
    ...     cookbook = Reference(schema=ICookbook)
    ...     instructions = Text(title=u"How to prepare the recipe.",
    ...         required=True)
    ...     private = Bool(title=u"Whether the public can see this recipe.",
    ...                    default=False)
    ...     def delete():
    ...         """Delete this recipe."""

    >>> IAuthor['favorite_recipe'].schema = IRecipe

Here's the interface for the 'set' objects that manage the authors,
cookbooks, and dishes. The inconsistent naming is intentional.

    >>> from lazr.restful.interfaces import ITraverseWithGet
    >>> class ITestDataSet(ITestDataObject, ITraverseWithGet):
    ...     """A marker interface."""

    >>> class IAuthorSet(ITestDataSet):
    ...     def getAllAuthors(self):
    ...         "Get all authors."
    ...
    ...     def get(self, request, name):
    ...         "Retrieve a single author by name."

    >>> class ICookbookSet(ITestDataSet):
    ...     def getAll(self):
    ...         "Get all cookbooks."
    ...
    ...     def get(self, request, name):
    ...         "Retrieve a single cookbook by name."
    ...
    ...     def findRecipes(self, name):
    ...         "Find recipes with a given name, across cookbooks."

    >>> class IDishSet(ITestDataSet):
    ...     def getAll(self):
    ...         "Get all dishes."
    ...
    ...     def get(self, request, name):
    ...         "Retrieve a single dish by name."


Here are simple implementations of IAuthor, IComment, ICookbook, IDish,
and IRecipe.

The web service uses the standard Zope protocol for mapping URLs to
object. So a URL is mapped to an object using the IPublishTraverse
interface, and the URL of an object is found by using the IAbsoluteURL
interface.


    >>> from urllib import quote
    >>> from zope.component import (
    ...     adapts, getSiteManager, getMultiAdapter)
    >>> from zope.interface import implements
    >>> from zope.publisher.interfaces import IPublishTraverse, NotFound
    >>> from zope.publisher.interfaces.browser import IBrowserRequest
    >>> from zope.security.checker import CheckerPublic
    >>> from zope.traversing.browser.interfaces import IAbsoluteURL
    >>> from lazr.restful.security import protect_schema

    >>> class BaseAbsoluteURL:
    ...     """A basic, extensible implementation of IAbsoluteURL."""
    ...     implements(IAbsoluteURL)
    ...
    ...     def __init__(self, context, request):
    ...         self.context = context
    ...         self.request = request
    ...
    ...     def __str__(self):
    ...         return "http://api.cookbooks.dev/beta/" + self.context.path
    ...
    ...     __call__ = __str__

    >>> sm = getSiteManager()
    >>> sm.registerAdapter(
    ...     BaseAbsoluteURL, [ITestDataObject, IBrowserRequest],
    ...     IAbsoluteURL)

    >>> class Author:
    ...     implements(IAuthor)
    ...     def __init__(self, name):
    ...         self.name = name
    ...         self.favorite_recipe = None
    ...         self.popularity = 1
    ...
    ...     @property
    ...     def path(self):
    ...         return 'authors/' + quote(self.name)

    >>> protect_schema(Author, IAuthor, write_permission=CheckerPublic)

    >>> class Comment:
    ...     implements(IComment)
    ...
    ...     def __init__(self, target, text):
    ...         self.target = target
    ...         self.text = text
    ...         self.target.comments.append(self)
    ...

    >>> protect_schema(Comment, IComment, write_permission=CheckerPublic)

    >>> class CommentAbsoluteURL(BaseAbsoluteURL):
    ...     """Code for generating the URL to a comment.
    ...
    ...     The URL is based on the URL of the ICommentTarget on which
    ...     this is a comment.
    ...     """
    ...     adapts(IComment, IBrowserRequest)
    ...
    ...     def __str__(self):
    ...         base = getMultiAdapter((self.context.target, request),
    ...                                IAbsoluteURL)()
    ...         return base + "/comments/%d" % (
    ...             self.context.target.comments.index(self.context)+1)
    ...     __call__ = __str__
    >>> sm.registerAdapter(CommentAbsoluteURL)

    >>> class Cookbook:
    ...     implements(ICookbook)
    ...     def __init__(self, name, author, cuisine=None):
    ...         self.name = name
    ...         self.author = author
    ...         self.recipes = []
    ...         self.comments = []
    ...         self.cuisine = cuisine
    ...         self.cover = None
    ...
    ...     @property
    ...     def path(self):
    ...         return 'cookbooks/' + quote(self.name)
    ...
    ...     def removeRecipe(self, recipe):
    ...         self.recipes.remove(recipe)

    >>> protect_schema(Cookbook, ICookbook, write_permission=CheckerPublic)

    >>> from urllib import unquote
    >>> class CookbookTraversal:
    ...     implements(IPublishTraverse)
    ...     adapts(ICookbook, IBrowserRequest)
    ...
    ...     traversing = None
    ...
    ...     def __init__(self, context, request):
    ...         self.context = context
    ...
    ...     def publishTraverse(self, request, name):
    ...         name = unquote(name)
    ...         if self.traversing is not None:
    ...             return getattr(self, 'traverse_' + self.traversing)(name)
    ...         elif name in ['comments', 'recipes']:
    ...             self.traversing = name
    ...             return self
    ...         else:
    ...             raise NotFound(self.context, name)
    ...
    ...     def traverse_comments(self, name):
    ...         try:
    ...             return self.context.comments[int(name)-1]
    ...         except (IndexError, TypeError, ValueError):
    ...             raise NotFound(self.context, 'comments/' + name)
    ...
    ...     def traverse_recipes(self, name):
    ...         name = unquote(name)
    ...         for recipe in self.context.recipes:
    ...             if recipe.dish.name == name:
    ...                 return recipe
    ...         raise NotFound(self.context, 'recipes/' + name)
    >>> protect_schema(CookbookTraversal, IPublishTraverse)
    >>> sm.registerAdapter(CookbookTraversal)

    >>> class Dish:
    ...     implements(IDish)
    ...     def __init__(self, name):
    ...         self.name = name
    ...         self.recipes = []
    ...     @property
    ...     def path(self):
    ...         return 'dishes/' + quote(self.name)
    ...     def removeRecipe(self, recipe):
    ...         self.recipes.remove(recipe)

    >>> protect_schema(Dish, IDish, write_permission=CheckerPublic)

    >>> class Recipe:
    ...     implements(IRecipe)
    ...     path = ''
    ...     def __init__(self, id, cookbook, dish, instructions,
    ...                  private=False):
    ...         self.id = id
    ...         self.cookbook = cookbook
    ...         self.cookbook.recipes.append(self)
    ...         self.dish = dish
    ...         self.dish.recipes.append(self)
    ...         self.instructions = instructions
    ...         self.comments = []
    ...         self.private = private
    ...     def delete(self):
    ...         self.cookbook.removeRecipe(self)
    ...         self.dish.removeRecipe(self)

    >>> protect_schema(Recipe, IRecipe, read_permission='zope.View',
    ...                write_permission=CheckerPublic)

    >>> class RecipeAbsoluteURL(BaseAbsoluteURL):
    ...     """Code for generating the URL to a recipe.
    ...
    ...     The URL is based on the URL of the cookbook to which
    ...     this recipe belongs.
    ...     """
    ...     adapts(IRecipe, IBrowserRequest)
    ...
    ...     def __str__(self):
    ...         base = getMultiAdapter((self.context.cookbook, request),
    ...                                IAbsoluteURL)()
    ...         return base + "/recipes/%s" % quote(self.context.dish.name)
    ...     __call__ = __str__
    >>> sm.registerAdapter(RecipeAbsoluteURL)

    >>> class RecipeTraversal:
    ...     adapts(IRecipe, IBrowserRequest)
    ...     implements(IPublishTraverse)
    ...
    ...     saw_comments = False
    ...
    ...     def __init__(self, context, request):
    ...         self.context = context
    ...
    ...     def publishTraverse(self, request, name):
    ...         name = unquote(name)
    ...         if self.saw_comments:
    ...             try:
    ...                 return self.context.comments[int(name)-1]
    ...             except (IndexError, TypeError, ValueError):
    ...                 raise NotFound(self.context, 'comments/' + name)
    ...         elif name == 'comments':
    ...             self.saw_comments = True
    ...             return self
    ...         else:
    ...             raise NotFound(self.context, name)
    >>> protect_schema(RecipeTraversal, IPublishTraverse)
    >>> sm.registerAdapter(RecipeTraversal)

Here are the "model objects" themselves:

    >>> A1 = Author(u"Julia Child")
    >>> A2 = Author(u"Irma S. Rombauer")
    >>> A3 = Author(u"James Beard")
    >>> AUTHORS = [A1, A2, A3]

    >>> C1 = Cookbook(u"Mastering the Art of French Cooking", A1)
    >>> C2 = Cookbook(u"The Joy of Cooking", A2)
    >>> C3 = Cookbook(u"James Beard's American Cookery", A3)
    >>> COOKBOOKS = [C1, C2, C3]

    >>> D1 = Dish("Roast chicken")
    >>> C1_D1 = Recipe(1, C1, D1, u"You can always judge...")
    >>> C2_D1 = Recipe(2, C2, D1, u"Draw, singe, stuff, and truss...")
    >>> C3_D1 = Recipe(3, C3, D1, u"A perfectly roasted chicken is...")

    >>> D2 = Dish("Baked beans")
    >>> C2_D2 = Recipe(4, C2, D2, "Preheat oven to...")
    >>> C3_D2 = Recipe(5, C3, D2, "Without doubt the most famous...", True)

    >>> D3 = Dish("Foies de voilaille en aspic")
    >>> C1_D3 = Recipe(6, C1, D3, "Chicken livers sauteed in butter...")

    >>> COM1 = Comment(C2_D1, "Clear and concise.")
    >>> COM2 = Comment(C2, "A kitchen staple.")

    >>> A1.favorite_recipe = C1_D1
    >>> A2.favorite_recipe = C2_D2
    >>> A3.favorite_recipe = C3_D2

Here's a simple CookbookSet with a predefined list of cookbooks.

    >>> from lazr.restful.simple import TraverseWithGet
    >>> class CookbookSet(BaseAbsoluteURL, TraverseWithGet):
    ...     implements(ICookbookSet)
    ...     path = 'cookbooks'
    ...
    ...     def __init__(self):
    ...         self.cookbooks = COOKBOOKS
    ...
    ...     def newCookbook(self, author_name, title, cuisine):
    ...         authors = AuthorSet()
    ...         author = authors.get(None, author_name)
    ...         if author is None:
    ...             author = authors.newAuthor(author_name)
    ...         cookbook = Cookbook(title, author, cuisine)
    ...         self.cookbooks.append(cookbook)
    ...         return cookbook
    ...
    ...     def getAll(self):
    ...         return self.cookbooks
    ...
    ...     def get(self, request, name):
    ...         match = [c for c in self.cookbooks if c.name == name]
    ...         if len(match) > 0:
    ...             return match[0]
    ...         return None
    ...
    ...     def findRecipes(self, name):
    ...         """Find recipes for a given dish across cookbooks."""
    ...         matches = []
    ...         for c in self.cookbooks:
    ...             for r in c.recipes:
    ...                 if r.dish.name == name:
    ...                     matches.append(r)
    ...                     break
    ...         # A somewhat arbitrary and draconian bit of error handling
    ...         # for the sake of demonstration.
    ...         if len(matches) == 0:
    ...             raise ValueError("No matches for %s" % name)
    ...         return matches

    >>> protect_schema(CookbookSet, ICookbookSet)
    >>> sm.registerUtility(CookbookSet(), ICookbookSet)

Here's a simple AuthorSet with predefined authors.

    >>> class AuthorSet(BaseAbsoluteURL, TraverseWithGet):
    ...     implements(IAuthorSet)
    ...     path = 'authors'
    ...
    ...     def __init__(self):
    ...         self.authors = AUTHORS
    ...
    ...     def newAuthor(self, name):
    ...         author = Author(name)
    ...         self.authors.append(author)
    ...         return author
    ...
    ...     def getAllAuthors(self):
    ...         return self.authors
    ...
    ...     def get(self, request, name):
    ...         match = [p for p in self.authors if p.name == name]
    ...         if len(match) > 0:
    ...             return match[0]
    ...         return None

    >>> sm.registerAdapter(
    ...     TraverseWithGet, [ITestDataObject, IBrowserRequest])
    >>> protect_schema(AuthorSet, IAuthorSet)
    >>> sm.registerUtility(AuthorSet(), IAuthorSet)

Here's a vocabulary of authors, for a field that presents a Choice
among authors.

    >>> from zope.schema.interfaces import IVocabulary
    >>> class AuthorVocabulary:
    ...     implements(IVocabulary)
    ...     def __iter__(self):
    ...         """Iterate over the authors."""
    ...         return AuthorSet().getAllAuthors().__iter__()
    ...
    ...     def __len__(self):
    ...         """Return the number of authors."""
    ...         return len(AuthorSet().getAllAuthors())
    ...
    ...     def getTerm(self, name):
    ...         """Retrieve an author by name."""
    ...         return AuthorSet().get(name)

Finally, a simple DishSet with predefined dishes.

    >>> class DishSet(BaseAbsoluteURL, TraverseWithGet):
    ...     implements(IDishSet)
    ...     path = 'dishes'
    ...     def __init__(self):
    ...         self.dishes = [D1, D2, D3]
    ...
    ...     def getAll(self):
    ...         return self.dishes
    ...
    ...     def get(self, request, name):
    ...         match = [d for d in self.dishes if d.name == name]
    ...         if len(match) > 0:
    ...             return match[0]
    ...         return None

    >>> protect_schema(DishSet, IDishSet)
    >>> sm.registerUtility(DishSet(), IDishSet)

========
Security
========

The webservice uses the normal zope.security API to check for
permission. For this example, let's register a simple policy that
denies access to private recipes.

    >>> from zope.security.permission import Permission
    >>> from zope.security.management import setSecurityPolicy
    >>> from zope.security.simplepolicies import PermissiveSecurityPolicy
    >>> from zope.security.proxy import removeSecurityProxy

    >>> sm.registerUtility(Permission('zope.View'), name='zope.View')

    >>> class SimpleSecurityPolicy(PermissiveSecurityPolicy):
    ...     def checkPermission(self, permission, object):
    ...         if IRecipe.providedBy(object):
    ...             return not removeSecurityProxy(object).private
    ...         else:
    ...             return True

    >>> setSecurityPolicy(SimpleSecurityPolicy)
    <class ...>

Web Service Infrastructure Initialization
=========================================

The lazr.restful package contains a set of default adapters and
definitions to implement the web service.

    >>> from zope.configuration import xmlconfig
    >>> zcmlcontext = xmlconfig.string("""
    ... <configure xmlns="http://namespaces.zope.org/zope">
    ...   <include package="lazr.restful" file="basic-site.zcml"/>
    ...   <utility
    ...       factory="lazr.restful.example.base.filemanager.FileManager" />
    ... </configure>
    ... """)

A IWebServiceConfiguration utility is also expected to be defined which
defines common configuration option for the webservice.

    >>> from lazr.restful import directives
    >>> from lazr.restful.interfaces import IWebServiceConfiguration
    >>> from lazr.restful.simple import BaseWebServiceConfiguration
    >>> from lazr.restful.testing.webservice import WebServiceTestPublication

    >>> class WebServiceConfiguration(BaseWebServiceConfiguration):
    ...     hostname = 'api.cookbooks.dev'
    ...     use_https = False
    ...     active_versions = ['beta', 'devel']
    ...     code_revision = 'test'
    ...     max_batch_size = 100
    ...     directives.publication_class(WebServiceTestPublication)
    ...     first_version_with_total_size_link = 'devel'

    >>> from grokcore.component.testing import grok_component
    >>> ignore = grok_component(
    ...     'WebServiceConfiguration', WebServiceConfiguration)

    >>> from zope.component import getUtility
    >>> webservice_configuration = getUtility(IWebServiceConfiguration)

We also need to define a marker interface for each version of the web
service, so that incoming requests can be marked with the appropriate
version string. The configuration above defines two versions, 'beta'
and 'devel'.

    >>> from lazr.restful.interfaces import IWebServiceClientRequest
    >>> class IWebServiceRequestBeta(IWebServiceClientRequest):
    ...     pass

    >>> class IWebServiceRequestDevel(IWebServiceClientRequest):
    ...     pass

    >>> versions = ((IWebServiceRequestBeta, 'beta'),
    ...             (IWebServiceRequestDevel, 'devel'))

    >>> from lazr.restful import register_versioned_request_utility
    >>> for cls, version in versions:
    ...     register_versioned_request_utility(cls, version)


Defining the resources
======================

lazr.restful provides an interface, ``IEntry``, used by an individual model
object exposed through a specific resource. This interface defines only one
attribute ``schema`` which should contain a schema describing the data fields
available in the entry. The same kind of fields defined by a model interface
like ``IRecipe``. It is expected that the entry adapter also provides that
schema itself.

If there's not much to an interface, you can expose it through the web service
exactly as it's defined, by defining a class that inherits from both the
interface and ``IEntry``. Since ``IAuthor`` and ``IComment`` are so simple, we
can define ``IAuthorEntry`` and ``ICommentEntry`` very simply.

The only extra and unusual step we have to take is to annotate the interfaces
with human-readable names for the objects we're exposing.

    >>> from zope.interface import taggedValue
    >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME
    >>> class IAuthorEntry(IAuthor, IEntry):
    ...     """The part of an author we expose through the web service."""
    ...     taggedValue(
    ...         LAZR_WEBSERVICE_NAME,
    ...         dict(
    ...             singular="author", plural="authors",
    ...             publish_web_link=True))

    >>> class ICommentEntry(IComment, IEntry):
    ...     """The part of a comment we expose through the web service."""
    ...     taggedValue(
    ...         LAZR_WEBSERVICE_NAME,
    ...         dict(
    ...             singular="comment", plural="comments",
    ...             publish_web_link=True))

Most of the time, it doesn't work to expose to the web service the same data
model we expose internally. Usually there are fields we don't want to expose,
synthetic fields we do want to expose, fields we want to expose as a different
type under a different name, and so on. This is why we have ``IEntry`` in the
first place: the ``IEntry`` interface defines the interface we _do_ want to
expose through the web service.

The reason we can't just define ``IDishEntry(IDish, IEntry)`` is that
``IDish`` defines the "recipes" collection as an ``Attribute``. ``Attribute``
is about as generic as "object", and doesn't convey any information about what
kind of object is in the collection, or even that "recipes" is a collection at
all. To expose the corresponding field to the web service we use
``CollectionField``.

    >>> from lazr.restful.fields import CollectionField
    >>> class IDishEntry(IEntry):
    ...     "The part of a dish that we expose through the web service."
    ...     recipes = CollectionField(value_type=Reference(schema=IRecipe))
    ...     taggedValue(
    ...         LAZR_WEBSERVICE_NAME,
    ...         dict(
    ...             singular="dish", plural="dishes",
    ...             publish_web_link=True))

In the following code block we define an interface that exposes the underlying
``Recipe``'s name but not its ID. References to associated objects (like the
recipe's cookbook) are represented with the ``zope.schema.Object`` type: this
makes it possible to serve a link from a recipe to its cookbook.

    >>> class IRecipeEntry(IEntry):
    ...     "The part of a recipe that we expose through the web service."
    ...     cookbook = Reference(schema=ICookbook)
    ...     dish = Reference(schema=IDish)
    ...     instructions = Text(title=u"Name", required=True)
    ...     comments = CollectionField(value_type=Reference(schema=IComment))
    ...     taggedValue(
    ...         LAZR_WEBSERVICE_NAME,
    ...         dict(
    ...             singular="recipe", plural="recipes",
    ...             publish_web_link=True))

    >>> from lazr.restful.fields import ReferenceChoice
    >>> class ICookbookEntry(IEntry):
    ...     name = TextLine(title=u"Name", required=True)
    ...     cuisine = TextLine(title=u"Cuisine", required=False, default=None)
    ...     author = ReferenceChoice(
    ...         schema=IAuthor, vocabulary=AuthorVocabulary())
    ...     recipes = CollectionField(value_type=Reference(schema=IRecipe))
    ...     comments = CollectionField(value_type=Reference(schema=IComment))
    ...     cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.")
    ...     taggedValue(
    ...         LAZR_WEBSERVICE_NAME,
    ...         dict(
    ...             singular="cookbook", plural="cookbooks",
    ...             publish_web_link=True))

The ``author`` field is a choice between ``Author`` objects. To make sure
that the ``Author`` objects are properly marshalled to JSON, we need to
define an adapter to ``IFieldMarshaller``.

    >>> from zope.schema.interfaces import IChoice
    >>> from lazr.restful.marshallers import (
    ...     ObjectLookupFieldMarshaller)
    >>> from lazr.restful.interfaces import (
    ...     IFieldMarshaller, IWebServiceClientRequest)
    >>> sm.registerAdapter(
    ...     ObjectLookupFieldMarshaller,
    ...     [IChoice, IWebServiceClientRequest, AuthorVocabulary],
    ...     IFieldMarshaller)

Implementing the resources
==========================

Here's the implementation of ``IAuthorEntry``: a simple decorator on the
original model object. It subclasses ``Entry``, a simple base class that
defines a constructor. (See http://pypi.python.org/pypi/lazr.delegates for
more on ``delegates()``.)

    >>> from zope.component import adapts
    >>> from zope.interface.verify import verifyObject
    >>> from lazr.delegates import delegates
    >>> from lazr.restful import Entry
    >>> from lazr.restful.testing.webservice import FakeRequest

    >>> from UserDict import UserDict
    >>> class FakeDict(UserDict):
    ...     def __init__(self, interface):
    ...         UserDict.__init__(self)
    ...         self.interface = interface
    ...     def __getitem__(self, key):
    ...         return self.interface

    >>> class AuthorEntry(Entry):
    ...     """An author, as exposed through the web service."""
    ...     adapts(IAuthor)
    ...     delegates(IAuthorEntry)
    ...     schema = IAuthorEntry
    ...     # This dict is normally generated by lazr.restful, but since we
    ...     # create the adapters manually here, we need to do the same for
    ...     # this dict.
    ...     _orig_interfaces = FakeDict(IAuthor)

    >>> request = FakeRequest()
    >>> verifyObject(IAuthorEntry, AuthorEntry(A1, request))
    True

The ``schema`` attribute points to the interface class that defines the
attributes exposed through the web service. Above, ``schema`` is
``IAuthorEntry``, which exposes only ``name``.

``IEntry`` also defines an invariant that enforces that it can be adapted to
the interface defined in the schema attribute. This is usually not a problem,
since the schema is usually the interface itself.

    >>> IAuthorEntry.validateInvariants(AuthorEntry(A1, request))

But the invariant will complain if that isn't true.

    >>> class InvalidAuthorEntry(Entry):
    ...     delegates(IAuthorEntry)
    ...     schema = ICookbookEntry

    >>> verifyObject(IAuthorEntry, InvalidAuthorEntry(A1, request))
    True
    >>> IAuthorEntry.validateInvariants(InvalidAuthorEntry(A1, request))
    Traceback (most recent call last):
      ...
    Invalid: InvalidAuthorEntry doesn't provide its ICookbookEntry schema.

Other entries are defined similarly.

    >>> class CookbookEntry(Entry):
    ...     """A cookbook, as exposed through the web service."""
    ...     delegates(ICookbookEntry)
    ...     schema = ICookbookEntry
    ...     # This dict is normally generated by lazr.restful, but since we
    ...     # create the adapters manually here, we need to do the same for
    ...     # this dict.
    ...     _orig_interfaces = FakeDict(ICookbook)

    >>> class DishEntry(Entry):
    ...     """A dish, as exposed through the web service."""
    ...     delegates(IDishEntry)
    ...     schema = IDishEntry
    ...     # This dict is normally generated by lazr.restful, but since we
    ...     # create the adapters manually here, we need to do the same for
    ...     # this dict.
    ...     _orig_interfaces = FakeDict(IDish)

    >>> class CommentEntry(Entry):
    ...     """A comment, as exposed through the web service."""
    ...     delegates(ICommentEntry)
    ...     schema = ICommentEntry
    ...     # This dict is normally generated by lazr.restful, but since we
    ...     # create the adapters manually here, we need to do the same for
    ...     # this dict.
    ...     _orig_interfaces = FakeDict(IComment)

    >>> class RecipeEntry(Entry):
    ...     delegates(IRecipeEntry)
    ...     schema = IRecipeEntry
    ...     # This dict is normally generated by lazr.restful, but since we
    ...     # create the adapters manually here, we need to do the same for
    ...     # this dict.
    ...     _orig_interfaces = FakeDict(IRecipe)

We need to register these entries as a multiadapter adapter from
(e.g.) ``IAuthor`` and ``IWebServiceClientRequest`` to (e.g.)
``IAuthorEntry``. In ZCML a registration would look like this.

    <adapter for="my.app.rest.IAuthor
                  lazr.restful.interfaces.IWebServiceClientRequest"
             factory="my.app.rest.AuthorEntry" />

Since we're in the middle of a Python example we can do the equivalent
in Python code for each entry class:

    >>> for entry_class, adapts_interface, provided_interface in [
    ...     [AuthorEntry, IAuthor, IAuthorEntry],
    ...     [CookbookEntry, ICookbook, ICookbookEntry],
    ...     [DishEntry, IDish, IDishEntry],
    ...     [CommentEntry, IComment, ICommentEntry],
    ...     [RecipeEntry, IRecipe, IRecipeEntry]]:
    ...         sm.registerAdapter(
    ...             entry_class, [adapts_interface, IWebServiceClientRequest],
    ...             provided=provided_interface)

lazr.restful also defines an interface and a base class for collections of
objects. I'll use it to expose the ``AuthorSet`` collection and other
top-level collections through the web service. A collection must define a
method called find(), which returns the model objects in the collection.

    >>> from lazr.restful import Collection
    >>> from lazr.restful.interfaces import ICollection

    >>> class AuthorCollection(Collection):
    ...     """A collection of authors, as exposed through the web service."""
    ...
    ...     entry_schema = IAuthorEntry
    ...
    ...     def find(self):
    ...        """Find all the authors."""
    ...        return self.context.getAllAuthors()

    >>> sm.registerAdapter(AuthorCollection,
    ...                   (IAuthorSet, IWebServiceClientRequest),
    ...                   provided=ICollection)

    >>> verifyObject(ICollection, AuthorCollection(AuthorSet(), request))
    True

    >>> class CookbookCollection(Collection):
    ...     """A collection of cookbooks, as exposed through the web service.
    ...     """
    ...     adapts(ICookbookSet)
    ...
    ...     entry_schema = ICookbookEntry
    ...
    ...     def find(self):
    ...        """Find all the cookbooks."""
    ...        return self.context.getAll()
    >>> sm.registerAdapter(CookbookCollection,
    ...                   (ICookbookSet, IWebServiceClientRequest),
    ...                   provided=ICollection)

    >>> class DishCollection(Collection):
    ...     """A collection of dishes, as exposed through the web service."""
    ...     adapts(IDishSet)
    ...
    ...     entry_schema = IDishEntry
    ...
    ...     def find(self):
    ...        """Find all the dishes."""
    ...        return self.context.getAll()

    >>> sm.registerAdapter(DishCollection,
    ...                   (IDishSet, IWebServiceClientRequest),
    ...                   provided=ICollection)

Like ``Entry``, ``Collection`` is a simple base class that defines a
constructor. The ``entry_schema`` attribute gives a ``Collection`` class
knowledge about what kind of entry it's supposed to contain.

    >>> DishCollection.entry_schema
    <InterfaceClass __builtin__.IDishEntry>

We also need to define a collection of the recipes associated with a cookbook.
We say that the collection of recipes is scoped to a cookbook. Scoped
collections adapters are looked for based on the type of the scope, and the
type of the entries contained in the scoped collection. There is a default
``ScopedCollection`` adapter that works whenever the scoped collection is
available as an iterable attribute of the context.

    >>> from lazr.restful.interfaces import IScopedCollection

    >>> def scope_collection(parent, child, name):
    ...     """A helper method that simulates a scoped collection lookup."""
    ...     parent_entry = getMultiAdapter((parent, request), IEntry)
    ...     child_entry = getMultiAdapter((child, request), IEntry)
    ...     scoped = getMultiAdapter((parent_entry, child_entry, request),
    ...                               IScopedCollection)
    ...     scoped.relationship = parent_entry.schema.get(name)
    ...     return scoped

The default adapter works just fine with the collection of recipes for
a cookbook.

    >>> scoped_collection = scope_collection(C1, C1_D1, 'recipes')
    >>> scoped_collection
    <lazr.restful...ScopedCollection...>

Like a regular collection, a scoped collection knows what kind of object is
inside it. Recall that the 'recipes' collection of a cookbook was defined as
one that contains objects with a schema of ``IRecipe``. This information is
available to the ``ScopedCollection`` object.

    >>> scoped_collection.entry_schema
    <InterfaceClass __builtin__.IRecipeEntry>

Field ordering
--------------

When an entry's fields are modified, it's important that the
modifications happen in a deterministic order, to minimize (or at
least make deterministic) bad interactions between fields. The helper
function get_entry_fields_in_write_order() handles this.

Ordinarily, fields are written to in the same order they are found in
the underlying schema.

    >>> author_entry = getMultiAdapter((A1, request), IEntry)
    >>> from lazr.restful._resource import get_entry_fields_in_write_order
    >>> def print_fields_in_write_order(entry):
    ...     for name, field in get_entry_fields_in_write_order(entry):
    ...         print name

    >>> print_fields_in_write_order(author_entry)
    name
    favorite_recipe
    popularity

The one exception is if a field is wrapped in a subclass of the
Passthrough class defined by the lazr.delegates library. Classes
generated through lazr.restful's annotations use a Passthrough
subclass to control a field that triggers complex logic when its value
changes. To minimize the risk of bad interactions, all the simple
fields are changed before any of the complex fields.

Here's a simple subclass of Passthrough.

    >>> from lazr.delegates import Passthrough
    >>> class MyPassthrough(Passthrough):
    ...     pass

When we replace 'favorite_recipe' with an instance of this subclass,
that field shows up at the end of the list of fields.

    >>> old_favorite_recipe = AuthorEntry.favorite_recipe
    >>> AuthorEntry.favorite_recipe = MyPassthrough('favorite_recipe', A1)
    >>> print_fields_in_write_order(author_entry)
    name
    popularity
    favorite_recipe

When we replace 'name' with a Passthrough subclass, it also shows up
at the end--but it still shows up before 'favorite_recipe', because it
comes before 'favorite_recipe' in the schema.

    >>> old_name = AuthorEntry.name
    >>> AuthorEntry.name = MyPassthrough('name', A1)
    >>> print_fields_in_write_order(author_entry)
    popularity
    name
    favorite_recipe

Cleanup to restore the old AuthorEntry implementation:

    >>> AuthorEntry.favorite_recipe = old_favorite_recipe
    >>> AuthorEntry.name = old_name

Custom operations
=================

The ``CookbookSet`` class defines a method called 'findRecipes'. This is
exposed through the cookbook collection resource as a custom operation called
``find_recipes``. Each custom operation is implemented as a class that
implements ``IResourceGETOperation``.

    >>> from lazr.restful import ResourceGETOperation
    >>> from zope.publisher.interfaces.http import IHTTPApplicationRequest
    >>> from lazr.restful.fields import Reference
    >>> from lazr.restful.interfaces import IResourceGETOperation
    >>> class FindRecipesOperation(ResourceGETOperation):
    ...    """An operation that searches for recipes across cookbooks."""
    ...    implements(IResourceGETOperation)
    ...    adapts(ICookbookSet, IHTTPApplicationRequest)
    ...
    ...    params = [ TextLine(__name__='name') ]
    ...    return_type = CollectionField(value_type=Reference(schema=IRecipe))
    ...
    ...    def call(self, name):
    ...        try:
    ...            return self.context.findRecipes(name)
    ...        except ValueError, e:
    ...            self.request.response.setStatus(400)
    ...            return str(e)

To register the class we just defined as implementing the ``find_recipes``
operation, we need to register it as a named adapter providing
``IResourceGETOperation`` for the ``ICookbookSet`` interface.

    >>> sm.registerAdapter(FindRecipesOperation, name="find_recipes")

The same underlying method is exposed through the recipe entry
resource as a custom operation called ``find_similar_recipes``.

    >>> class FindSimilarRecipesOperation(ResourceGETOperation):
    ...    """Finds recipes with the same name."""
    ...    implements(IResourceGETOperation)
    ...    adapts(IRecipe, IHTTPApplicationRequest)
    ...    params = []
    ...    return_type = CollectionField(value_type=Reference(schema=IRecipe))
    ...
    ...    def call(self):
    ...        try:
    ...            return CookbookSet().findRecipes(self.context.dish.name)
    ...        except AssertionError, e:
    ...            self.request.response.setStatus(400)
    ...            return str(e)

    >>> sm.registerAdapter(
    ...     FindSimilarRecipesOperation, name="find_similar_recipes")

Named GET operations are read-only operations like searches, but
resources can also expose named write operations, through POST. Here's
a named factory operation for creating a new cookbook.

    >>> from lazr.restful.interfaces import IResourcePOSTOperation
    >>> from lazr.restful import ResourcePOSTOperation
    >>> class CookbookFactoryOperation(ResourcePOSTOperation):
    ...     """An operation that creates a new cookbook."""
    ...     implements(IResourcePOSTOperation)
    ...     adapts(ICookbookSet, IHTTPApplicationRequest)
    ...     params = (
    ...         TextLine(__name__='author_name'),
    ...         TextLine(__name__='title'),
    ...         TextLine(
    ...             __name__='cuisine', default=u'Brazilian', required=False),
    ...     )
    ...     return_type = Reference(schema=IRecipe)
    ...
    ...     def call(self, author_name, title, cuisine):
    ...         cookbook = CookbookSet().newCookbook(
    ...             author_name, title, cuisine)
    ...         self.request.response.setStatus(201)
    ...         self.request.response.setHeader(
    ...             "Location", absoluteURL(cookbook, self.request))
    ...         return cookbook

    >>> sm.registerAdapter(
    ...     CookbookFactoryOperation, name="create_cookbook")

Here's a named POST operation that's not a factory operation: it makes a
cookbook's cuisine sound more interesting.

    >>> class MakeMoreInterestingOperation(ResourcePOSTOperation):
    ...     implements(IResourcePOSTOperation)
    ...     adapts(ICookbook, IHTTPApplicationRequest)
    ...     params = ()
    ...     return_type = None
    ...     send_modification_event = True
    ...
    ...     def call(self):
    ...         cookbook = self.context
    ...         cookbook.cuisine = "Nouvelle " + cookbook.cuisine

    >>> sm.registerAdapter(
    ...     MakeMoreInterestingOperation, name="make_more_interesting")

Operations are also used to implement DELETE on entries. This code
implements DELETE for IRecipe objects.

    >>> from lazr.restful.interfaces import IResourceDELETEOperation
    >>> from lazr.restful import ResourceDELETEOperation
    >>> class RecipeDeleteOperation(ResourceDELETEOperation):
    ...     implements(IResourceDELETEOperation)
    ...     adapts(IRecipe, IHTTPApplicationRequest)
    ...     params = ()
    ...     return_type = None
    ...
    ...     def call(self):
    ...         self.context.delete()
    >>> sm.registerAdapter(
    ...     RecipeDeleteOperation, name="")


Resource objects
================

lazr.restful ``Resource`` objects are the objects that actually handle
incoming HTTP requests. There are a few very common types of HTTP resources,
and LAZR defines classes for some of them. For instance, there's the
"collection" resource that responds to GET (to get the items in the
collection) and POST (to invoke named operations on the collection).
lazr.restful implements this as a ``CollectionResource`` which uses the HTTP
arguments to drive ``Collection`` methods like find().

Of course, a ``CollectionResource`` has to expose a collection _of_
something. That's why each ``CollectionResource`` is associated with some
concrete implementation of ``ICollection``, like ``AuthorCollection``. All you
have to do is define the behaviour of the collection, and
``CollectionResource`` takes care of exposing the collection through HTTP.

Similarly, you can implement ``RecipeEntry`` to the ``IEntry`` interface, and
expose it through the web as an ``EntryResource``.

The Service Root Resource
=========================

How are these ``Resource`` objects connected to the web? Through the
``ServiceRootResource``. This is a special resource that represents the
root of the object tree.

    >>> from lazr.restful.interfaces import IServiceRootResource
    >>> from lazr.restful import ServiceRootResource
    >>> from zope.traversing.browser.interfaces import IAbsoluteURL

    >>> class MyServiceRootResource(ServiceRootResource, TraverseWithGet):
    ...     implements(IAbsoluteURL)
    ...     path = ''
    ...
    ...     top_level_names = {
    ...         'dishes': DishSet(),
    ...         'cookbooks': CookbookSet(),
    ...         'authors': AuthorSet()}
    ...
    ...     def get(self, request, name):
    ...         return self.top_level_names.get(name)

It's the responsibility of each web service to provide an implementation of
``IAbsoluteURL`` and ``IPublishTraverse`` for their service root resource.

    >>> sm.registerAdapter(
    ...     BaseAbsoluteURL, [MyServiceRootResource, IBrowserRequest])

    >>> app = MyServiceRootResource()
    >>> sm.registerUtility(app, IServiceRootResource)

If you call the service root resource, and pass in an HTTP request, it
will act as though you had performed a GET on the URL
'http://api.cookbooks.dev/beta/'.

    >>> webservice_configuration.root = app
    >>> from lazr.restful.testing.webservice import (
    ...     create_web_service_request)

    >>> request = create_web_service_request('/beta/')
    >>> ignore = request.traverse(app)

The response document is a JSON document full of links to the
top-level collections of authors, cookbooks, and dishes. It's the
'home page' for the web service.

    >>> import simplejson
    >>> response = app(request)
    >>> representation = simplejson.loads(unicode(response))

    >>> representation["authors_collection_link"]
    u'http://api.cookbooks.dev/beta/authors'

    >>> representation["cookbooks_collection_link"]
    u'http://api.cookbooks.dev/beta/cookbooks'

    >>> representation["dishes_collection_link"]
    u'http://api.cookbooks.dev/beta/dishes'

The standard ``absoluteURL()`` function can be used to generate URLs to
content objects published on the web service. It works for the web service
root, so long as you've given it an ``IAbsoluteURL`` implementation.

    >>> from zope.traversing.browser import absoluteURL
    >>> absoluteURL(app, request)
    'http://api.cookbooks.dev/beta/'

WADL documents
==============

Every resource can serve a WADL representation of itself. The main
WADL document is the WADL representation of the server root. It
describes the capabilities of the web service as a whole.

    >>> wadl_headers = {'HTTP_ACCEPT' : 'application/vd.sun.wadl+xml'}
    >>> wadl_request = create_web_service_request(
    ...     '/beta/', environ=wadl_headers)
    >>> wadl_resource = wadl_request.traverse(app)
    >>> print wadl_resource(wadl_request)
    <?xml version="1.0"?>
    <!DOCTYPE...
    <wadl:application ...>
    ...
    </wadl:application>

If the resources are improperly configured, the WADL can't be generated.
Here's an example, where ``DishCollection`` is registered as an adapter twice.
Earlier it was registered as the adapter for ``IDishSet``; here it's also
registered as the adapter for ``IAuthorSet``. The WADL generation doesn't know
whether to describe ``DishCollection`` using the named operations defined
against ``IAuthorSet`` or the named operations defined against ``IDishSet``,
so there's an ``AssertionError``.

    >>> sm.registerAdapter(DishCollection, [IAuthorSet], ICollection)
    >>> print wadl_resource(wadl_request)
    Traceback (most recent call last):
    ...
    AssertionError: There must be one (and only one) adapter
    from DishCollection to ICollection.

Collection resources
====================

The default root navigation defined in our model contains the top-level
Set objects that should be published. When these sets are published on
the web service, they will we wrapped in the appropriate
``CollectionResource``.

The following example is equivalent to requesting
'http://api.cookbooks.dev/cookbooks/'. The code will traverse to the
``CookbookSet`` published normally at '/cookbooks' and it will be wrapped into
a ``CollectionResource``.

    >>> request = create_web_service_request('/beta/cookbooks')
    >>> collection = request.traverse(app)
    >>> collection
    <lazr.restful...CollectionResource object ...>

Calling the collection resource yields a JSON document, which can be
parsed with standard tools.

    >>> def load_json(s):
    ...     """Convert a JSON string to Unicode and then load it."""
    ...     return simplejson.loads(unicode(s))

    >>> representation = load_json(collection())
    >>> representation['resource_type_link']
    u'http://api.cookbooks.dev/beta/#cookbooks'

Pagination
==========

``Collections`` are paginated and served one page at a time. This particular
collection is small enough to fit on one page; it's only got three entries.

    >>> sorted(representation.keys())
    [u'entries', u'resource_type_link', u'start', u'total_size']
    >>> len(representation['entries'])
    3
    >>> representation['total_size']
    3

But if we ask for a page size of two, we can see how pagination
works. Here's page one, with two cookbooks on it.

    >>> request = create_web_service_request(
    ...     '/beta/cookbooks', environ={'QUERY_STRING' : 'ws.size=2'})
    >>> collection = request.traverse(app)
    >>> representation = load_json(collection())

    >>> sorted(representation.keys())
    [u'entries', u'next_collection_link', u'resource_type_link',
     u'start', u'total_size']
    >>> representation['next_collection_link']
    u'http://api.cookbooks.dev/beta/cookbooks?ws.start=2&ws.size=2'
    >>> len(representation['entries'])
    2
    >>> representation['total_size']
    3

Follow the ``next_collection_link`` and you'll end up at page two, which
has the last cookbook on it.

    >>> request = create_web_service_request(
    ...     '/beta/cookbooks',
    ...     environ={'QUERY_STRING' : 'ws.start=2&ws.size=2'})
    >>> collection = request.traverse(app)
    >>> representation = load_json(collection())

    >>> sorted(representation.keys())
    [u'entries', u'prev_collection_link', u'resource_type_link',
     u'start', u'total_size']
    >>> representation['prev_collection_link']
    u'http://api.cookbooks.dev/beta/cookbooks?ws.start=0&ws.size=2'
    >>> len(representation['entries'])
    1

Custom operations
=================

A collection may also expose a number of custom operations through
GET. The cookbook collection exposes a custom GET operation called
``find_recipes``, which searches for recipes with a given name across
cookbooks.

    >>> request = create_web_service_request(
    ...    '/beta/cookbooks',
    ...    environ={'QUERY_STRING' :
    ...             'ws.op=find_recipes&name=Roast%20chicken'})
    >>> operation_resource = request.traverse(app)
    >>> chicken_recipes = load_json(operation_resource())
    >>> sorted([c['instructions'] for c in chicken_recipes['entries']])
    [u'A perfectly roasted chicken is...',
     u'Draw, singe, stuff, and truss...',
     u'You can always judge...']

Custom operations may include custom error checking. Error messages
are passed along to the client.

    >>> request = create_web_service_request(
    ...    '/beta/cookbooks',
    ...    environ={'QUERY_STRING' :
    ...             'ws.op=find_recipes&name=NoSuchRecipe'})
    >>> operation_resource = request.traverse(app)
    >>> print operation_resource()
    No matches for NoSuchRecipe

Collections may also support named POST operations. These requests
have two effects on the server side: they modify the dataset, and they
may also trigger event notifications. Here are two simple handlers set
up to print a message whenever we modify a cookbook or the cookbook
set.

    >>> def modified_cookbook(object, event):
    ...     """Print a message when triggered."""
    ...     print "You just modified a cookbook."

    >>> from lazr.lifecycle.interfaces import IObjectModifiedEvent
    >>> from lazr.restful.testing.event import TestEventListener
    >>> cookbook_listener = TestEventListener(
    ...     ICookbook, IObjectModifiedEvent, modified_cookbook)

    >>> def modified_cookbook_set(object, event):
    ...     """Print a message when triggered."""
    ...     print "You just modified the cookbook set."

Here we create a new cookbook for an existing author. Because the
operation's definition doesn't set send_modified_event to True, no
event will be sent and modified_cookbook_set() won't be called.

    >>> body = ("ws.op=create_cookbook&title=Beard%20on%20Bread&"
    ...         "author_name=James%20Beard")
    >>> request = create_web_service_request(
    ...     '/beta/cookbooks', 'POST', body,
    ...     {'CONTENT_TYPE' : 'application/x-www-form-urlencoded'})
    >>> operation_resource = request.traverse(app)
    >>> result = operation_resource()

    >>> request.response.getStatus()
    201
    >>> request.response.getHeader('Location')
    'http://api.cookbooks.dev/beta/cookbooks/Beard%20on%20Bread'

Here we create a cookbook for a new author.

    >>> body = ("ws.op=create_cookbook&title=Everyday%20Greens&"
    ...         "author_name=Deborah%20Madison")
    >>> request = create_web_service_request(
    ...     '/beta/cookbooks', 'POST', body,
    ...     {'CONTENT_TYPE' : 'application/x-www-form-urlencoded'})
    >>> operation_resource = request.traverse(app)
    >>> result = operation_resource()
    >>> request.response.getStatus()
    201
    >>> request.response.getHeader('Location')
    'http://api.cookbooks.dev/beta/cookbooks/Everyday%20Greens'

The new Author object is created implicitly and is published as a
resource afterwards.

    >>> path = '/beta/authors/Deborah%20Madison'
    >>> request = create_web_service_request(path)
    >>> author = request.traverse(app)
    >>> load_json(author())['name']
    u'Deborah Madison'

Here we modify a cookbook's cuisine using a named operation. Because
this operation's definition does set send_modified_event to True, an
event will be sent and modified_cookbook_set() will be called.

    >>> body = "ws.op=make_more_interesting"
    >>> request = create_web_service_request(
    ...     '/beta/cookbooks/Everyday%20Greens', 'POST', body,
    ...     {'CONTENT_TYPE' : 'application/x-www-form-urlencoded'})
    >>> operation_resource = request.traverse(app)
    >>> result = operation_resource()
    You just modified a cookbook.
    >>> request.response.getStatus()
    200

    >>> path = '/beta/cookbooks/Everyday%20Greens'
    >>> request = create_web_service_request(path)
    >>> cookbook = request.traverse(app)
    >>> load_json(cookbook())['cuisine']
    u'Nouvelle Brazilian'


Entry resources
===============

The collection resource is a list of entries. Each entry has some
associated information (like 'name'), a ``self_link`` (the URL to the
entry's resource), and possibly links to associated resources.

    >>> import operator
    >>> request = create_web_service_request('/beta/cookbooks')
    >>> collection = request.traverse(app)
    >>> representation = load_json(collection())
    >>> entries = sorted(representation['entries'],
    ...                  key=operator.itemgetter('name'))
    >>> entries[0]['self_link']
    u'http://api.cookbooks.dev/beta/cookbooks/Beard%20on%20Bread'

Regular data fields are exposed with their given names. The 'name'
field stays 'name'.

    >>> entries[0]['name']
    u'Beard on Bread'

Fields that are references to other objects -- ``Object``, ``Reference``, and
``ReferenceChoice`` -- are exposed as links to those objects. Each cookbook
has such a link to its author.

    >>> entries[0]['author_link']
    u'http://api.cookbooks.dev/beta/authors/James%20Beard'

Fields that are references to externally hosted files (Bytes) are also
exposed as links to those files. Each cookbook has such a link to its
cover image.

    >>> entries[0]['cover_link']
    u'http://api.cookbooks.dev/beta/cookbooks/Beard%20on%20Bread/cover'

Fields that are references to collections of objects are exposed as
links to those collections. Each cookbook has such a link to its
recipes.

    >>> entries[0]['recipes_collection_link']
    u'http://api.cookbooks.dev/beta/cookbooks/Beard%20on%20Bread/recipes'

Calling the ``CollectionResource`` object makes it process the incoming
request. Since this is a GET request, calling the resource publishes the
resource to the web. A ``CollectionResource`` is made up of a bunch of
``EntryResources``, and the base ``EntryResource`` class knows how to use the
entry schema class (in this case, ``IRecipeEntry``) to publish a JSON
document.

The same way collections are wrapped into ``CollectionResource``, navigating
to an object that has an ``IEntry`` adapter, will wrap it into an
``EntryResource``.

For instance, creating a new cookbook and making a request to its URL
will wrap it into an ``EntryResource``.

    >>> body = ("ws.op=create_cookbook&title=Feijoada&"
    ...         "author_name=Fernando%20Yokota")
    >>> request = create_web_service_request(
    ...     '/beta/cookbooks', 'POST', body,
    ...     {'CONTENT_TYPE' : 'application/x-www-form-urlencoded'})
    >>> operation_resource = request.traverse(app)
    >>> result = operation_resource()
    >>> request.response.getHeader('Location')
    'http://api.cookbooks.dev/beta/cookbooks/Feijoada'
    >>> request = create_web_service_request('/beta/cookbooks/Feijoada')
    >>> feijoada_resource = request.traverse(app)
    >>> feijoada_resource
    <lazr.restful...EntryResource object ...>
    >>> feijoada = load_json(feijoada_resource())

Notice how the request above didn't specify the book's cuisine,
but since that is not a required field our application used the default
value (Brazilian) specified in ``CookbookFactoryOperation`` for it.

    >>> sorted(feijoada.items())
    [(u'author_link',
      u'http://api.cookbooks.dev/beta/authors/Fernando%20Yokota'),
     (u'comments_collection_link',
      u'http://api.cookbooks.dev/beta/cookbooks/Feijoada/comments'),
     (u'cover_link',
      u'http://api.cookbooks.dev/beta/cookbooks/Feijoada/cover'),
     (u'cuisine', u'Brazilian'),
     (u'http_etag', u'...'),
     (u'name', u'Feijoada'),
     (u'recipes_collection_link',
      u'http://api.cookbooks.dev/beta/cookbooks/Feijoada/recipes'),
     (u'resource_type_link',
      u'http://api.cookbooks.dev/beta/#cookbook'),
     (u'self_link', u'http://api.cookbooks.dev/beta/cookbooks/Feijoada')]

You can also traverse from an entry to an item in a scoped collection:

    >>> request = create_web_service_request(
    ...     quote('/beta/cookbooks/The Joy of Cooking/recipes/Roast chicken'))
    >>> chicken_recipe_resource = request.traverse(app)
    >>> chicken_recipe = load_json(chicken_recipe_resource())
    >>> sorted(chicken_recipe.items())
    [(u'comments_collection_link',
     u'http://api...Joy%20of%20Cooking/recipes/Roast%20chicken/comments'),
    (u'cookbook_link',
     u'http://api.cookbooks.dev/beta/cookbooks/The%20Joy%20of%20Cooking'),
    (u'dish_link', u'http://api.cookbooks.dev/beta/dishes/Roast%20chicken'),
    (u'http_etag', u'...'),
    (u'instructions', u'Draw, singe, stuff, and truss...'),
    (u'self_link',
     u'http://api.../The%20Joy%20of%20Cooking/recipes/Roast%20chicken')]

Another example traversing to a comment:

    >>> roast_chicken_comments_url = quote(
    ... '/beta/cookbooks/The Joy of Cooking/recipes/Roast chicken/comments')
    >>> request = create_web_service_request(roast_chicken_comments_url)
    >>> comments_resource = request.traverse(app)

    >>> comments = load_json(comments_resource())
    >>> [c['text'] for c in comments['entries']]
    [u'Clear and concise.']

    >>> request = create_web_service_request(
    ...     roast_chicken_comments_url + '/1')
    >>> comment_one_resource = request.traverse(app)
    >>> comment_one = load_json(comment_one_resource())
    >>> sorted(comment_one.items())
    [(u'http_etag', u'...'),
     (u'resource_type_link', u'http://api.cookbooks.dev/beta/#comment'),
     (u'self_link',
      u'http://api...Joy%20of%20Cooking/recipes/Roast%20chicken/comments/1'),
     (u'text', u'Clear and concise.')]

An entry may expose a number of custom operations through GET. The
recipe entry exposes a custom GET operation called
'find_similar_recipes', which searches for recipes with the same name
across cookbooks.

    >>> request = create_web_service_request(
    ...     '/beta/cookbooks/The%20Joy%20of%20Cooking/recipes/Roast%20chicken',
    ...     environ={'QUERY_STRING' : 'ws.op=find_similar_recipes'})
    >>> operation_resource = request.traverse(app)
    >>> chicken_recipes = load_json(operation_resource())
    >>> sorted([c['instructions'] for c in chicken_recipes['entries']])
    [u'A perfectly roasted chicken is...',
     u'Draw, singe, stuff, and truss...',
     u'You can always judge...']

Named operation return values
=============================

The return value of a named operation is serialized to a JSON data
structure, and the response's Content-Type header is set to
application/json. These examples show how different return values are
serialized.

    >>> class DummyOperation(ResourceGETOperation):
    ...
    ...     params = ()
    ...     result = None
    ...     return_type = None
    ...
    ...     def call(self):
    ...         return self.result

    >>> def make_dummy_operation_request(result):
    ...    request = create_web_service_request('/beta/')
    ...    ignore = request.traverse(app)
    ...    operation = DummyOperation(None, request)
    ...    operation.result = result
    ...    return request, operation

Scalar Python values like strings and booleans are serialized as you'd
expect.

    >>> request, operation = make_dummy_operation_request("A string.")
    >>> print operation()
    "A string."
    >>> request.response.getStatus()
    200
    >>> print request.response.getHeader('Content-Type')
    application/json

    >>> request, operation = make_dummy_operation_request(True)
    >>> operation()
    'true'

    >>> request, operation = make_dummy_operation_request(10)
    >>> operation()
    '10'

    >>> request, operation = make_dummy_operation_request(None)
    >>> operation()
    'null'

    >>> request, operation = make_dummy_operation_request(1.3)
    >>> operation()
    '1.3'

When a named operation returns an object that has an ``IEntry``
implementation, the object is serialized to a JSON hash.

    >>> request, operation = make_dummy_operation_request(D2)
    >>> operation()
    '{...}'

A named operation can return a data structure that incorporates
objects with ``IEntry`` implementations. Here's a dictionary that contains
a ``Dish`` object. The ``Dish`` object is serialized as a JSON dictionary
within the larger dictionary.

    >>> request, operation = make_dummy_operation_request({'dish': D2})
    >>> operation()
    '{"dish": {...}}'

When a named operation returns a list or tuple of objects, we serve
the whole thing as a JSON list.

    >>> request, operation = make_dummy_operation_request([1,2,3])
    >>> operation()
    '[1, 2, 3]'

    >>> request, operation = make_dummy_operation_request((C1_D1, C2_D1))
    >>> operation()
    '[{...}, {...}]'

When a named operation returns a non-builtin object that provides the
iterator protocol, we don't return the whole list. The object probably
provides access to a potentially huge dataset, like a list of database
results. In this case we do the same thing we do when serving a
collection resource. We fetch one batch of results and represent it as
a JSON hash containing a list of entries.

    >>> class DummyResultSet(object):
    ...     results = [C1_D1, C2_D1]
    ...
    ...     def __iter__(self):
    ...         return iter(self.results)
    ...
    ...     def __len__(self):
    ...         return len(self.results)
    ...
    ...     def __getitem__(self, index):
    ...         return self.results[index]

    >>> recipes = DummyResultSet()
    >>> request, operation = make_dummy_operation_request(recipes)
    >>> operation()
    '{"total_size": 2, ... "entries": [{...}, {...}]}'

When a named operation returns an object that has an ``ICollection``
implementation, the result is similar: we return a JSON hash describing one
batch from the collection.

    >>> request, operation = make_dummy_operation_request(DishSet())
    >>> operation()
    '{"total_size": ..., "start": ...}'

If the return value can't be converted into JSON, you'll get an
exception.

    >>> request, operation = make_dummy_operation_request(object())
    >>> operation()
    Traceback (most recent call last):
    ...
    TypeError: Could not serialize object <object...> to JSON.

    >>> request, operation = make_dummy_operation_request(
    ...     {'anobject' : object()})
    >>> operation()
    Traceback (most recent call last):
    ...
    TypeError: Could not serialize object {'anobject': <object...>} to JSON.

    >>> request, operation = make_dummy_operation_request([object()])
    >>> operation()
    Traceback (most recent call last):
    ...
    TypeError: Could not serialize object [<object object...>] to JSON.

ETags
=====

Every entry resource has a short opaque string called an ETag that
summarizes the resource's current state. The ETag is sent as the
response header 'ETag'.

    >>> julia_object = A1
    >>> julia_url = quote('/beta/authors/Julia Child')
    >>> get_request = create_web_service_request(julia_url)
    >>> ignored = get_request.traverse(app)()
    >>> etag_original = get_request.response.getHeader('ETag')

The ETag is different across revisions of the software, but within a
release it'll always the same for a given resource with a given state.

    >>> get_request = create_web_service_request(julia_url)
    >>> ignored = get_request.traverse(app)()
    >>> etag_after_get = get_request.response.getHeader('ETag')

    >>> etag_after_get == etag_original
    True

A client can use a previously obtained ETag as the value of
If-None-Match when making a request. If the ETags match, it means the
resource hasn't changed since the client's last request. The server
sends a response code of 304 ("Not Modified") instead of sending the
same representation again.

First, let's define a helper method to request a specific entry
resource, and gather the entity-body and the response object into an
easily accessible data structure.

    >>> def get_julia(etag=None):
    ...     headers = {'CONTENT_TYPE' : 'application/json'}
    ...     if etag is not None:
    ...         headers['HTTP_IF_NONE_MATCH'] = etag
    ...     get_request = create_web_service_request(
    ...         julia_url, environ=headers)
    ...     entity_body = get_request.traverse(app)()
    ...     return dict(entity_body=entity_body,
    ...                 response=get_request.response)

    >>> print get_julia(etag_original)['response'].getStatus()
    304

If the ETags don't match, the server assumes the client has an old
representation, and sends the new representation.

    >>> print get_julia('bad etag')['entity_body']
    {...}

Change the state of the resource, and the ETag changes.

    >>> julia_object.favorite_recipe = C2_D2
    >>> etag_after_modification = get_julia()['response'].getHeader('ETag')

    >>> etag_after_modification == etag_original
    False

The client can't modify read-only fields, but they might be modified
behind the scenes. If one of them changes, the ETag will change.

    >>> julia_object.popularity = 5
    >>> etag_after_readonly_change = get_julia()['response'].getHeader(
    ...     'ETag')
    >>> etag_after_readonly_change == etag_original
    False

compensate_for_mod_compress_etag_modification
---------------------------------------------

Apache's mod_compress transparently modifies outgoing ETags, but
doesn't remove the modifications when the ETags are sent back in. The
configuration setting 'compensate_for_mod_compress_etag_modification'
makes lazr.restful compensate for this behavior, so that you can use
mod_compress to save bandwidth.

Different versions of mod_compress modify outgoing ETags in different
ways. lazr.restful handles both cases.

   >>> etag = get_julia()['response'].getHeader('ETag')
   >>> modified_etag_1 = etag + '-gzip'
   >>> modified_etag_2 = etag[:-1] + '-gzip' + etag[-1]

Under normal circumstances, lazr.restful won't recognize an ETag
modified by mod_compress.

    >>> print get_julia(modified_etag_1)['entity_body']
    {...}

When 'compensate_for_mod_compress_etag_modification' is set,
lazr.restful will recognize an ETag modified by mod_compress.

    >>> c = webservice_configuration
    >>> print c.compensate_for_mod_compress_etag_modification
    False
    >>> c.compensate_for_mod_compress_etag_modification = True

    >>> print get_julia(modified_etag_1)['response'].getStatus()
    304

    >>> print get_julia(modified_etag_2)['response'].getStatus()
    304

Of course, that doesn't mean lazr.restful will recognize any random
ETag.

    >>> print get_julia(etag + "-not-gzip")['entity_body']
    {...}

Cleanup.

    >>> c.compensate_for_mod_compress_etag_modification = False

Resource Visibility
===================

Certain resources might not be visible to every user. In this example,
certain recipes have been designated as private and can't be seen by
unauthenticated users. For demonstration purposes, the recipe for
"Baked beans" in "James Beard's American Cookery" has been marked as
private. An unauthorized attempt to GET this resource will result in
an error.

    >>> private_recipe_url = quote(
    ...     "/beta/cookbooks/James Beard's American Cookery/recipes/"
    ...     "Baked beans")
    >>> get_request = create_web_service_request(private_recipe_url)
    >>> recipe_resource = get_request.traverse(app)
    Traceback (most recent call last):
    ...
    Unauthorized: (<Recipe object...>, 'dish', ...)

The recipe will not show up in collections:

    >>> recipes_url = quote(
    ...     "/beta/cookbooks/James Beard's American Cookery/recipes")
    >>> get_request = create_web_service_request(recipes_url)
    >>> collection_resource = get_request.traverse(app)
    >>> collection = load_json(collection_resource())

The web service knows about two recipes from James Beard's American
Cookery, but an unauthorized user can only see one of them.

    >>> len(collection['entries'])
    1

Note that the 'total_size' of the collection is slightly inaccurate,
having been generated before invisible recipes were filtered out.

    >>> collection['total_size']
    2

As it happens, the author "James Beard" has his 'favorite_recipe'
attribute set to the "Baked beans" recipe. But an unauthorized user
can't see anything about that recipe, not even its URL.

    >>> beard_url = quote('/beta/authors/James Beard')
    >>> get_request = create_web_service_request(beard_url)
    >>> author_resource = get_request.traverse(app)
    >>> author = load_json(author_resource())

The author's name is public information, so it's visible. But the link
to his favorite recipe has been redacted.

    >>> author['name']
    u'James Beard'
    >>> author['favorite_recipe_link']
    u'tag:launchpad.net:2008:redacted'

It's possible to use a representation that contains redacted
information when sending a PUT or PATCH request back to the
server. The server will know that the client isn't actually trying to
set the field value to 'tag:launchpad.net:2008:redacted'.

    >>> headers = {'CONTENT_TYPE' : 'application/json'}
    >>> body = simplejson.dumps(author)
    >>> put_request = create_web_service_request(
    ...     beard_url, body=body, environ=headers, method='PUT')
    >>> put_request.traverse(app)()
    '{...}'

And since no special permission is necessary to _change_ a person's
'favorite_recipe', it's possible to set it to a visible recipe using
PUT, even when its current value is redacted.

    >>> author['favorite_recipe_link'] = 'http://' + quote(
    ...     'api.cookbooks.dev/beta/cookbooks/'
    ...     'The Joy of Cooking/recipes/Roast chicken')
    >>> body = simplejson.dumps(author)
    >>> put_request = create_web_service_request(
    ...     beard_url, body=body, environ=headers, method='PUT')
    >>> put_request.traverse(app)()
    '{...}'

After that PUT, James Beard's 'favorite_recipe' attribute is no longer
redacted. It's the value set by the PUT request.

    >>> get_request = create_web_service_request(beard_url)
    >>> author_resource = get_request.traverse(app)
    >>> author = load_json(author_resource())
    >>> author['favorite_recipe_link']
    u'http://api.cookbooks.dev/beta/cookbooks/The%20Joy%20of%20Cooking/recipes/Roast%20chicken'

Finally, you can't set an attribute to a value that you wouldn't have
permission to see:

    >>> author['favorite_recipe_link'] = (
    ...     'http://api.cookbooks.dev' + private_recipe_url)
    >>> body = simplejson.dumps(author)
    >>> put_request = create_web_service_request(
    ...     beard_url, body=body, environ=headers, method='PUT')
    >>> print put_request.traverse(app)()
    (<Recipe object...>, 'dish', ...)

    >>> print put_request.response.getStatus()
    401

Stored file resources
=====================

Binary files, such as the covers of cookbooks, are stored on an external
server, but they have addresses within the web service. The mapping of binary
resources to the actual hosting of them is handled through the
``IByteStorage`` interface. For this example, let's define simple
implementation that serves all files from the /files path.

    >>> from lazr.restful.interfaces import IByteStorage
    >>> from lazr.restful.example.base.interfaces import (
    ...     IFileManagerBackedByteStorage)
    >>> from lazr.restful.example.base.root import SimpleByteStorage
    >>> protect_schema(SimpleByteStorage, IFileManagerBackedByteStorage)
    >>> sm.registerAdapter(SimpleByteStorage, provided=IByteStorage)

A newly created cookbook has no cover.

    >>> cover_url = quote('/beta/cookbooks/The Joy of Cooking/cover')
    >>> get_request = create_web_service_request(cover_url)
    >>> file_resource = get_request.traverse(app)
    >>> file_resource()
    Traceback (most recent call last):
    ...
    NotFound: ... name: 'cover'

    >>> print C2.cover
    None

A cookbook can be given a cover with PUT.

    >>> headers = {'CONTENT_TYPE' : 'image/png'}
    >>> body = 'Pretend this is an image.'
    >>> put_request = create_web_service_request(
    ...     cover_url, body=body, environ=headers, method='PUT')
    >>> file_resource = put_request.traverse(app)
    >>> file_resource()

    >>> C2.cover.representation
    'Pretend...'

At this point it exists:

    >>> get_request = create_web_service_request(cover_url)
    >>> file_resource = get_request.traverse(app)
    >>> file_resource()
    >>> get_request.response.getStatus()
    303
    >>> print get_request.response.getHeader('Location')
    http://cookbooks.dev/.../filemanager/0

The cover can be deleted with DELETE.

    >>> delete_request = create_web_service_request(
    ...     cover_url, method='DELETE')
    >>> file_resource = delete_request.traverse(app)
    >>> file_resource()

    >>> get_request = create_web_service_request(cover_url)
    >>> file_resource = get_request.traverse(app)
    >>> file_resource()
    Traceback (most recent call last):
    ...
    NotFound: ... name: 'cover'

    >>> print C2.cover
    None

Field resources
===============

An entry's primitive data fields are exposed as subordinate resources.

    >>> field_resource = create_web_service_request(
    ...     '/beta/cookbooks/The%20Joy%20of%20Cooking/name').traverse(app)
    >>> print field_resource()
    "The Joy of Cooking"

Requesting non available resources
==================================

If the user tries to traverse to a nonexistent object, the result is a
NotFound exception.

Requesting a non-existent top-level collection:

    >>> create_web_service_request('/beta/nosuchcollection').traverse(app)
    Traceback (most recent call last):
    ...
    NotFound: ... name: u'nosuchcollection'

Requesting a non-existent cookbook:

    >>> create_web_service_request('/beta/cookbooks/104').traverse(app)
    Traceback (most recent call last):
    ...
    NotFound: ... name: u'104'

Requesting a non-existent comment:

    >>> create_web_service_request(
    ...  '/beta/cookbooks/The%20Joy%20of%20Cooking/comments/10').traverse(app)
    Traceback (most recent call last):
    ...
    NotFound: ... name: u'comments/10'

Manipulating entries
====================

Most entry resources support write operations by responding to PATCH
requests. The entity-body of a PATCH request should be a JSON document
with new values for some of the entry's attributes. Basically, a set
of assertions about what the object *should* look like.

A PATCH request will automatically result in a modification event
being sent out about the modified object, which means that
modify_cookbook() will be run. Here, we modify the name and the
cuisine of one of the cookbooks. Note that the cuisine contains
non-ASCII characters.

    >>> headers = {'CONTENT_TYPE' : 'application/json'}
    >>> body = '''{"name" : "The Joy of Cooking (revised)",
    ...            "cuisine" : "\xd7\x97\xd7\x95\xd7\x9e\xd7\x95\xd7\xa1"}'''

    >>> patch_request = create_web_service_request(
    ...     '/beta/cookbooks/The%20Joy%20of%20Cooking', body=body,
    ...     environ=headers, method='PATCH')
    >>> joy_resource_patch = patch_request.traverse(app)
    >>> joy_resource_patch()
    You just modified a cookbook.
    ''

    >>> patch_request.response.getHeader('Location')
    'http://api.../cookbooks/The%20Joy%20of%20Cooking%20%28revised%29'

The new name is reflected in the cookbook's representation, and the
cookbook's URL has changed as well.

    >>> request = create_web_service_request(
    ...     '/beta/cookbooks/The%20Joy%20of%20Cooking%20%28revised%29')
    >>> joy_resource = request.traverse(app)
    >>> joy = load_json(joy_resource())
    >>> joy['name']
    u'The Joy of Cooking (revised)'

An entry that responds to PATCH will also respond to PUT. With PUT you
modify the document you got in response to a GET request, and send the
whole thing back to the server, whereas with PATCH you're creating a
new document that describes a subset of the entry's state.

Here, we use PUT to change the cookbook's name back to what it was
before. Note that we send the entire dictionary back to the
server. Note also that another modification event is sent out and
intercepted by the modified_cookbook() listener.

    >>> joy['name'] = 'The Joy of Cooking'
    >>> body = simplejson.dumps(joy)
    >>> put_request = create_web_service_request(
    ...     '/beta/cookbooks/The%20Joy%20of%20Cooking%20%28revised%29',
    ...     body=body, environ=headers, method='PUT')

    >>> joy_resource_put = put_request.traverse(app)
    >>> joy_resource_put()
    You just modified a cookbook.
    ''

Now that we've proved our point, let's disable the event handler so it
doesn't keep printing those messages.

    >>> cookbook_listener.unregister()

The cookbook's URL has changed back to what it was before.

    >>> put_request.response.getStatus()
    301
    >>> put_request.response.getHeader('Location')
    'http://api.cookbooks.dev/beta/cookbooks/The%20Joy%20of%20Cooking'

So has the cookbook's name.

    >>> joy_resource = create_web_service_request(
    ...     '/beta/cookbooks/The%20Joy%20of%20Cooking').traverse(app)
    >>> joy = load_json(joy_resource())
    >>> joy['name']
    u'The Joy of Cooking'

It's also possible to change the relationships between objects. Here,
we change a cookbook's author. Since all objects are identified by
their URLs, we make the change by modifying the cookbook's
'author_link' field to point to another author.

    >>> def change_joy_author(new_author_link, host='api.cookbooks.dev'):
    ...     representation = {'author_link' : new_author_link}
    ...     resource = create_web_service_request(
    ...         '/beta/cookbooks/The%20Joy%20of%20Cooking',
    ...         body=simplejson.dumps(representation), environ=headers,
    ...         method='PATCH', hostname=host).traverse(app)
    ...     return resource()
    >>> path = '/beta/authors/Julia%20Child'

    >>> change_joy_author(u'http://api.cookbooks.dev' + path)
    '{...}'

    >>> joy_resource = create_web_service_request(
    ...     '/beta/cookbooks/The%20Joy%20of%20Cooking').traverse(app)
    >>> joy = load_json(joy_resource())
    >>> joy['author_link']
    u'http://api.cookbooks.dev/beta/authors/Julia%20Child'

When identifying an object by URL, make sure the hostname of your URL
matches the hostname you're requesting. If they don't match, your
request will fail.

    >>> print change_joy_author(u'http://not.the.same.host' + path)
    author_link: No such object...

One possible source of hostname mismatches is the HTTP port. If the
web service is served from a strange port, you'll need to specify that
port in the URLs you send.

    >>> print change_joy_author(u'http://api.cookbooks.dev' + path,
    ...                         host='api.cookbooks.dev:9000')
    author_link: No such object...

    >>> print change_joy_author(u'http://api.cookbooks.dev:9000' + path,
    ...                         host='api.cookbooks.dev:9000')
    {...}

You don't have to specify the default port in the URLs you send, even
if you specified it when you made the request.

    >>> print change_joy_author(u'http://api.cookbooks.dev' + path,
    ...                         host='api.cookbooks.dev:80')
    {...}

    >>> print change_joy_author(u'http://api.cookbooks.dev:80' + path,
    ...                         host='api.cookbooks.dev')
    {...}

    >>> print change_joy_author(u'https://api.cookbooks.dev' + path,
    ...                         host='api.cookbooks.dev:443')
    author_link: No such object...

    >>> webservice_configuration.use_https = True
    >>> print change_joy_author(u'https://api.cookbooks.dev' + path,
    ...                         host='api.cookbooks.dev:443')
    {...}
    >>> webservice_configuration.use_https = False

If an entry has an IResourceDELETEOperation registered for it, you can
activate that operation and delete the entry by sending a DELETE
request.

    >>> from urllib import quote
    >>> recipe_url = quote('/beta/cookbooks/Mastering the Art of '
    ...     'French Cooking/recipes/Foies de voilaille en aspic')

Now you see it...

    >>> resource = create_web_service_request(
    ...     recipe_url, method='GET').traverse(app)
    >>> print resource()
    {...}

    >>> resource = create_web_service_request(
    ...     recipe_url, method='DELETE').traverse(app)
    >>> ignored = resource()

Now you don't.

    >>> resource = create_web_service_request(
    ...     recipe_url, method='GET').traverse(app)
    Traceback (most recent call last):
    ...
    NotFound: ... name: u'recipes/Foies de voilaille en aspic'


Within a template
=================

A number of TALES adapters give different views on resource
objects. The is_entry() function is a conditional that returns true when
given an object that can be adapted to IEntry.

    >>> from lazr.restful.testing.tales import test_tales
    >>> test_tales("context/webservice:is_entry", context=A1)
    True
    >>> test_tales("context/webservice:is_entry", context=AUTHORS)
    False

The json() function converts generic Python data structures to JSON,
as well as objects that can be adapted to IEntry. It converts markup
characters (<, >, &) into their respective Unicode escape sequences,
since entities within <script> tags are not expanded.

    >>> test_tales("context/webservice:json", context="foobar")
    '"foobar"'
    >>> test_tales("context/webservice:json", context=A1)
    '{"name": ...}'
    >>> test_tales("context/webservice:json", context="<foo&>")
    '"\\u003cfoo\\u0026\\u003e"'
