| Name | How map.resource enables controllers as services |
|---|---|
| Space | Pylons Cookbook |
| Section | Web services |
| Page | How map.resource enables controllers as services |
| Version | 1.0 |
| Status | Draft |
| Reviewed | False |
| Author(s) | Graham Higgins |
How map.resource enables controllers as services
In this section we describe the underlying principles and the use of the map.resource facility. We will introduce the notion of RESTful web services, briefly touching on polymorphism and demonstrate how you can take advantage of the benefits of a RESTful approach to structure your web services in a clean and scalable fashion.
A brief introduction to REST
REST is not a standard but an architectural style, a specific (and highly recommended) way of designing web services.
| Say something about Fielding, HTTPD and stateless protocols. How I explained REST to my wife: http://tomayko.com/writings/rest-to-my-wife |
The approach involves considering your controllers to be the fount of a set of conceptual resources. A resource could be a blog posting or an item in a product catalogue; some 'thing'.
Your service provides representations of these resources for people and programs to use. Typically, you might serve a web page carrying details of a product (a hard drive, say) i.e. you serve a representation of the thing, not send the actual thing itself. The representation can take different forms, depending on the intended use, for example: a web page in HTML or an RSS item in XML or a JSON-formatted string.
There is a small but useful set of verbs which can be applied to 'things': GET, POST, PUT and DELETE.
For example, consider a hypothetical 'thing' service, where 'things' are accessed by a numeric id. This might be a typical set of incoming requests:
GET /things/1756 POST /things/1756 PUT /things/1756 DELETE /things/1756
Which we can map into the corresponding URLs:
GET /things/1756 = /things/1756 POST /things/1756 = /things/1756 PUT /things/1756 = /things/1756/update DELETE /things/1756 = /things/1756/update
Simple and clean, scales very well. It will also benefit the consumers of your services, such as browser enhancements, desktop apps and search engines.
Pylons' support for creating RESTful web services
Pylons makes it very easy to create RESTful web services. The paster script comes with a restcontroller option which instantiates the controller with a special RestController class that is automatically populated with the requisite skeletal actions.
The Pylons RestController
The RestController command will create a REST-based Controller file for use with the map.resource REST-based dispatching. This template includes the methods that map.resource dispatches to in addition to doc strings for clarification on when the methods will be called.
The first argument should be the singular form of the REST resource. The second argument is the plural form of the word. If it's a nested controller, put the directory information in front as shown in the second example below.
Example usage:
myapp% paster restcontroller message messages
Creating myapp/myapp/controllers/messages.py
Creating myapp/myapp/tests/functional/test_messages.py
If you'd like to have controllers underneath a directory, just include the path as the controller name and the necessary directories will be created for you:
myapp% paster restcontroller admin/tracback admin/trackbacks
Creating myapp/controllers/admin
Creating myapp/myapp/controllers/admin/trackbacks.py
Creating myapp/myapp/tests/functional/test_admin_trackbacks.py
The contents of myapp/controllers/messages.py will look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | controllers/messages.py from myapp.lib.base import * class CommentsController(BaseController): """REST Controller styled on the Atom Publishing Protocol""" # To properly map this controller, ensure your config/routing.py file has # a resource setup: # map.resource('message', 'messages') def index(self, format='html'): """GET /: All items in the collection.""" # url_for('messages') pass def create(self): """POST /: Create a new item.""" # url_for('messages') pass def new(self, format='html'): """GET /new: Form to create a new item.""" # url_for('new_message') pass def update(self, id): """PUT /id: Update an existing item.""" # Forms posted to this method should contain a hidden field: # <input type="hidden" name="_method" value="PUT" /> # Or using helpers: # h.form(h.url_for('message', id=ID), method='put') # url_for('message', id=ID) pass def delete(self, id): """DELETE /id: Delete an existing item.""" # Forms posted to this method should contain a hidden field: # <input type="hidden" name="_method" value="DELETE" /> # Or using helpers: # h.form(h.url_for('message', id=ID), method='delete') # url_for('message', id=ID) pass def show(self, id, format='html'): """GET /id: Show a specific item.""" # url_for('message', id=ID) pass def edit(self, id, format='html'): """GET /id;edit: Form to edit an existing item.""" # url_for('edit_message', id=ID) pass |
How to use map.resource
To create the appropriate RESTful mapping, add a map statement to your config/routing.py file near the top like this:
1 | map.resource('message', 'messages') |
To make it easier to setup RESTful web services with Routes, there's a shortcut Mapper method that will setup a batch of routes for you along with conditions that will restrict them to specific HTTP methods. This is directly styled on the Rails version of map.resource, which was based heavily on the Atom Publishing Protocol.
The map.resource command creates a set of Routes for common operations on a collection of resources, individually referred to as 'members'. Consider the common case where you have a system that deals with users. In that case operations dealing with the entire group of users (or perhaps a subset) would be considered collection methods. Operations (or actions) that act on an individual member of that collection are considered member methods. These terms are important to remember as the options to map.resource rely on a clear understanding of collection actions vs. member actions.
1 | map.resource('message', 'messages') |
Will setup all the routes as if you had typed the following map commands:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | map.connect('messages', controller='messages', action='create', conditions=dict(method=['POST'])) map.connect('messages', 'messages', controller='messages', action='index', conditions=dict(method=['GET'])) map.connect('formatted_messages', 'messages.:(format)', controller='messages', action='index', conditions=dict(method=['GET'])) map.connect('new_message', 'messages/new', controller='messages', action='new', conditions=dict(method=['GET'])) map.connect('formatted_new_message', 'messages/new.:(format)', controller='messages', action='new', conditions=dict(method=['GET'])) map.connect('messages/:id', controller='messages', action='update', conditions=dict(method=['PUT'])) map.connect('messages/:id', controller='messages', action='delete', conditions=dict(method=['DELETE'])) map.connect('edit_message', 'messages/:(id)/edit', controller='messages', action='edit', conditions=dict(method=['GET'])) map.connect('formatted_edit_message', 'messages/:(id).:(format)/edit', controller='messages, action='edit', conditions=dict(method=['GET'])) map.connect('message', 'messages/:id', controller='messages', action='show', conditions=dict(method=['GET'])) map.connect('formatted_message', 'messages/:(id).:(format)', controller='messages', action='show', conditions=dict(method=['GET'])) |
The most important aspects of this is the following mapping that is established:
GET /messages -> messages.index() -> url_for('messages')
POST /messages -> messages.create() -> url_for('messages')
GET /messages/new -> messages.new() -> url_for('new_message')
PUT /messages/1 -> messages.update(id) -> url_for('message', id=1)
DELETE /messages/1 -> messages.delete(id) -> url_for('message', id=1)
GET /messages/1 -> messages.show(id) -> url_for('message', id=1)
GET /messages/1/edit -> messages.edit(id) -> url_for('edit_message', id=1)
The complete mapping based on the routes created above seems to be:
POST /messages -> messages.create() -> url_for('messages')
GET /messages -> messages.index() -> url_for('messages')
GET /messages.xml -> messages.index(format='xml') -> url_for('formatted_messages', format='xml')
GET /messages/new -> messages.new() -> url_for('new_message')
GET /messages/new.xml -> messages.new(format='xml') -> url_for('formatted_new_message', format='xml')
PUT /messages/1 -> messages.update(id) -> url_for('message', id=1)
DELETE /messages/1 -> messages.delete(id) -> url_for('message', id=1)
GET /messages/1/edit -> messages.edit(id) -> url_for('edit_message', id=1)
GET /messages/1.xml/edit -> messages.edit(id, format='xml') -> url_for('formatted_edit_message', id=1, format='xml')
GET /messages/1 -> messages.show(id) -> url_for('message', id=1)
GET /messages/1.xml -> messages.show(id, format='xml') -> url_for('formatted_message', id=1, format='xml')
Several of these methods map to functions intended to display forms. The new message method should be used to return a form allowing someone to create a new message, while it should POST to /messages. The edit message function should work similarly returning a form to edit a message, which then posts a PUT to the /messages/1 resource.
Generate routes for a controller resource
1 | resource(self, member_name, collection_name, **kwargs) ... |
The member_name name should be the appropriate singular version of the resource given your locale and used with members of the collection. The collection_name name will be used to refer to the resource collection methods and should be a plural version of the member_name argument. By default, the member_name name will also be assumed to map to a controller you create.
The concept of a web resource maps somewhat directly to 'CRUD' operations. The overlying things to keep in mind is that mapping a resource is about handling creating, viewing, and editing that resource.
Keywords
All keyword arguments are optional.
controller
If specified in the keyword args, the controller will be the actual controller used, but the rest of the naming conventions used for the route names and URL paths are unchanged.
collection
Additional action mappings used to manipulate/view the entire set of resources provided by the controller. Example:
1 2 3 | map.resource('message', collection={'rss':'GET'}) # GET /message;rss (maps to the rss action) # also adds named route "rss_message" |
member
Additional action mappings used to access an individual 'member' of this controllers resources. Example:
1 2 3 | map.resource('message', member={'mark':'POST'}) # POST /message/1;mark (maps to the mark action) # also adds named route "mark_message" |
new
Action mappings that involve dealing with a new member in the controller resources. Example:
1 2 3 | map.resource('message', new={'preview':'POST'}) # POST /message/new;preview (maps to the preview action) # also adds a url named "preview_new_message" |
path_prefix
Prepends the URL path for the Route with the path_prefix given. This is most useful for cases where you want to mix resources or relations between resources.
name_prefix
Prepends the route names that are generated with the name_prefix given. Combined with the path_prefix option, it's easy to generate route names and paths that represent resources that are in relations. Example:
1 2 3 4 | map.resource('message', controller='categories', path_prefix='/category/:category_id', name_prefix="category_") # GET /category/7/message/1 # has named route "category_message" |
For more information see the Routes documentation at http://routes.groovie.org/manual.html#restful-services
Comments (2)
Sep 18, 2010
askqiao says:
About map.resource mapping to map.connect: >map.connect('formatted_edit_me...About map.resource mapping to map.connect:
>map.connect('formatted_edit_message', 'messages/:(id).:(format)/edit', controller='messages',
> action='edit', conditions=dict(method=['GET']))
Does it means that id has a subroutine in syntax?
I'd try it in my env(pylons 1.0), it is represented as follows:
map.connect('formatted_edit_message', 'messages/:(id)/edit.:(format)', controller='messages',
action='edit', conditions=dict(method=['GET']))
I think it is right, means that method:edit has a subroutine but not id has a subroutine.
>map.connect('formatted_message', 'messages/:(id).:(format)', controller='messages', action='show',
> conditions=dict(method=['GET']))
It means that method:show name can be implicit called, but the same question as above that id has as subroutine.
Who can explain in detail? Thanks.
Nov 04, 2010
Matteo Dello Ioio says:
Hi, I'm new in Pylons. I have created a REST controller and I have added resourc...Hi, I'm new in Pylons.
I have created a REST controller and I have added resource mapping in routing.py, but I can't get the format parameter correctly.
If i call http://mysite/mycontroller/myaction/myid.json the format parameter is always html. O_o
The only way I get the format parameter correctly is in index action when I make this call: http://mysite/mycontroller.json
I think this is weird...
Someone can help me?
Thanks in advance.
p.s.
Pylons version 1.0