From 232a0d4ef0c1750e8b859c89c02061663532a6a0 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Wed, 3 Dec 2014 17:44:24 -0500 Subject: [PATCH] Add documentation for generic REST controllers. Fixes bug 1386837 Change-Id: I9cf30c7d55026d2364d7add3f7a9ca96b3348d31 --- docs/source/rest.rst | 82 ++++++++++++++++++++++++--- docs/source/routing.rst | 122 +++++++++++++++++++++++++++------------- 2 files changed, 157 insertions(+), 47 deletions(-) diff --git a/docs/source/rest.rst b/docs/source/rest.rst index 8432a86..6f6a01b 100644 --- a/docs/source/rest.rst +++ b/docs/source/rest.rst @@ -1,13 +1,81 @@ .. _rest: -Writing RESTful Web Services with Pecan -======================================= +Writing RESTful Web Services with Generic Controllers +===================================================== -If you need to write controllers to interact with objects, using the -:class:`~pecan.rest.RestController` may help speed things up. It follows the -Representational State Transfer Protocol, also known as REST, by routing the -standard HTTP verbs of ``GET``, ``POST``, ``PUT``, and ``DELETE`` to individual -methods. +Pecan simplifies RESTful web services by providing a way to overload URLs based +on the request method. For most API's, the use of `generic controller` +definitions give you everything you need to build out robust RESTful +interfaces (and is the *recommended* approach to writing RESTful web services +in pecan): + +:: + + from pecan import abort, expose + + # Note: this is *not* thread-safe. In real life, use a persistent data store. + BOOKS = { + '0': 'The Last of the Mohicans', + '1': 'Catch-22' + } + + + class BookController(object): + + def __init__(self, id_): + self.id_ = id_ + assert self.book + + @property + def book(self): + if self.id_ in BOOKS: + return dict(id=self.id_, name=BOOKS[self.id_]) + abort(404) + + # HTTP GET // + @expose(generic=True, template='json') + def index(self): + return self.book + + # HTTP PUT // + @index.when(method='PUT', template='json') + def index_PUT(self, **kw): + BOOKS[self.id_] = kw['name'] + return self.book + + # HTTP DELETE // + @index.when(method='DELETE', template='json') + def index_DELETE(self): + del BOOKS[self.id_] + return dict() + + + class RootController(object): + + @expose() + def _lookup(self, id_, *remainder): + return BookController(id_), remainder + + # HTTP GET / + @expose(generic=True, template='json') + def index(self): + return [dict(id=k, name=v) for k, v in BOOKS.items()] + + # HTTP POST / + @index.when(method='POST', template='json') + def index_POST(self, **kw): + id_ = len(BOOKS) + BOOKS[id_] = kw['name'] + return dict(id=id_, name=kw['name']) + + +Writing RESTful Web Services with RestController +================================================ + +.. _TurboGears2: http://turbogears.org + +For compatability with the TurboGears2_ library, Pecan also provides +a class-based solution to RESTful routing, :class:`~pecan.rest.RestController`: :: diff --git a/docs/source/routing.rst b/docs/source/routing.rst index 68a46ba..4b80472 100644 --- a/docs/source/routing.rst +++ b/docs/source/routing.rst @@ -106,7 +106,7 @@ Let's look at an example using ``template`` and ``content_type``: def hello(self): return {'msg': 'Hello!'} -You'll notice that we called :func:`~pecan.decoators.expose` three times, with +You'll notice that we called :func:`~pecan.decorators.expose` three times, with different arguments. :: @@ -136,6 +136,34 @@ use the ``text/html`` content type by default. * :ref:`pecan_decorators` +Routing Based on Request Method +------------------------------- + +The ``generic`` argument to :func:`~pecan.decorators.expose` provides support for overloading URLs +based on the request method. In the following example, the same URL can be +serviced by two different methods (one for handling HTTP ``GET``, another for +HTTP ``POST``) using `generic controllers`: + +:: + + from pecan import expose + + + class RootController(object): + + # HTTP GET / + @expose(generic=True, template='json') + def index(self): + return dict() + + # HTTP POST / + @index.when(method='POST', template='json') + def index_POST(self, **kw): + uuid = create_something() + return dict(uuid=uuid) + + + Pecan's Routing Algorithm ------------------------- @@ -148,44 +176,6 @@ these methods on your controller objects provides additional flexibility for processing all or part of a URL. -Setting a Return Status Code ----------------------------- - -Set a specific HTTP response code (such as ``201 Created``) by -modifying the ``status`` attribute of the response object. - -:: - - from pecan import expose, response - - class RootController(object): - - @expose('json') - def hello(self): - response.status = 201 - return {'foo': 'bar'} - -Use the utility function :func:`~pecan.core.abort` to raise HTTP errors. - -:: - - from pecan import expose, abort - - class RootController(object): - - @expose('json') - def hello(self): - abort(404) - - -:func:`~pecan.core.abort` raises an instance of -:class:`~webob.exc.WSGIHTTPException` which is used by Pecan to render -:default response bodies for HTTP errors. This exception is stored in -:the WSGI request environ at ``pecan.original_exception``, where it -:can be accessed later in the request cycle (by, for example, other -:middleware or :ref:`errors`). - - Routing to Subcontrollers with ``_lookup`` ------------------------------------------ @@ -268,7 +258,7 @@ a :func:`_route` method will enable you to have total control. Interacting with the Request and Response Object ------------------------------------------------- +================================================ For every HTTP request, Pecan maintains a :ref:`thread-local reference ` to the request and response object, ``pecan.request`` and @@ -295,6 +285,58 @@ directly, there may be situations where you want to access them, such as: * Manually rendering a response body +Specifying a Custom Response +---------------------------- + +Set a specific HTTP response code (such as ``203 Non-Authoritative Information``) by +modifying the ``status`` attribute of the response object. + +:: + + from pecan import expose, response + + class RootController(object): + + @expose('json') + def hello(self): + response.status = 203 + return {'foo': 'bar'} + +Use the utility function :func:`~pecan.core.abort` to raise HTTP errors. + +:: + + from pecan import expose, abort + + class RootController(object): + + @expose('json') + def hello(self): + abort(404) + + +:func:`~pecan.core.abort` raises an instance of +:class:`~webob.exc.WSGIHTTPException` which is used by Pecan to render +default response bodies for HTTP errors. This exception is stored in +the WSGI request environ at ``pecan.original_exception``, where it +can be accessed later in the request cycle (by, for example, other +middleware or :ref:`errors`). + +If you'd like to return an explicit response, you can do so using +:class:`~pecan.core.Response`: + +:: + + from pecan import expose, Response + + class RootController(object): + + @expose() + def hello(self): + return Response('Hello, World!', 202) + + + Extending Pecan's Request and Response Object ---------------------------------------------