Using the REST Architecture for Web Services
In 2000, Roy Thomas Fielding published the paper "Architectural Styles and the Design of Network-based Software Architectures" (http://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm). This paper described a software architecture style called REST (short for "Representational State Transfer") and explained how this architecture could be seen in the design of the Web. REST isn't tied to any particular technology or data formats. Instead, it offers a set of principles for designing web applications that take advantage of the fundamental design of the Web.
Applications that use the REST architectural style use the Web the way it was designed to be used. Instead of struggling to overcome the limitations of the Web, REST embraces those constraints to give developers an elegant, powerful approach to designing and thinking about web applications.
5.1.1. How Do I Do That?
Design your web application in terms of resources. Every URI on the Web identifies a resource. A resource may be any information or service, including a document, an image, or a web service that takes input data and performs some action. What information and services is your application going to provide? Each should be available through the Web at a logical URI. HTTP defines four actions that can be taken on resources: GET, POST, PUT, and DELETE. Web browsers generally support only two of these: GET, for retrieving the representation of a resource, and POST, for submitting data to a service for processing. But when designing services meant for use by non-browser clients you can take advantage of PUT and DELETE as well.
Example 5-1 shows how you can use the REST architecture to design a web application that's usable through both a web browser and web services. In this case, the application is a Wiki, a simple set of user-editable linked web pages.
Example 5-1. wiki.py
import re, pickle, os from twisted.web import server, resource, http reWikiWord = re.compile(r"(([A-Z][a-z]+){2,})") class WikiData(object): "class for managing Wiki data" def _ _init_ _(self, filename): self.filename = filename if os.path.exists(filename): self.pages = pickle.load(file(filename, 'r+b')) else: self.pages = {'WikiHome': 'This is your Wiki home page.'} def save(self): pickle.dump(self.pages, file(self.filename, 'w+b')) def hasPage(self, path): return self.pages.has_key(path) def listPages(self): return self.pages.keys( ) def getPage(self, path): return self.pages.get(path, '') def getRenderedPage(self, path): pageData = self.getPage(path) "get content with linked WikiWords" wikiWords = [match[0] for match in reWikiWord.findall(pageData)] for word in wikiWords: pageData = pageData.replace( word, "<a href="%s">%s</a>" % (word, word)) return pageData def setPage(self, path, content): self.pages[path] = content self.save( ) def deletePage(self, path): del(self.pages[path]) class RootResource(resource.Resource): def _ _init_ _(self, wikiData): self.wikiData = wikiData resource.Resource._ _init_ _(self) self.putChild('edit', EditFactory(self.wikiData)) self.putChild('save', SavePage(self.wikiData)) self.putChild('index', IndexPage(self.wikiData)) def getChild(self, path, request): return WikiPage(self.wikiData, path or 'WikiHome') class EditFactory(resource.Resource): def _ _init_ _(self, wikiData): self.wikiData = wikiData resource.Resource._ _init_ _(self) def getChild(self, path, request): return EditPage(self.wikiData, path) class EditPage(resource.Resource): def _ _init_ _(self, wikiData, path): self.wikiData = wikiData self.path = path resource.Resource._ _init_ _(self) def render(self, request): return """
Editing: %(path)s
Editing: %(path)s
%(data)s
""" % {'path':self.path, 'data':self.wikiData.getPage(self.path)} class SavePage(resource.Resource): def _ _init_ _(self, wikiData): self.wikiData = wikiData resource.Resource._ _init_ _(self) def render_POST(self, request): data = request.args['data'][0] path = request.args['path'][0] self.wikiData.setPage(path, data) request.redirect('/' + path) return "" def render_GET(self, request): return """ To add a page to the Wiki, send data to this page using HTTP POST. Arguments:
path
: the name of a Wiki pagedata
: the content to use for that page
""" class WikiPage(resource.Resource): def _ _init_ _(self, wikiData, path): self.wikiData = wikiData self.path = path resource.Resource._ _init_ _(self) def render_GET(self, request): if self.wikiData.hasPage(self.path): return """%s
<a href="/edit/%s">Edit Page</a> <a href="/index">Index</a> <a href="/">Home</a>
%s """ % (self.path, self.path, self.wikiData.getRenderedPage(self.path)) else: request.redirect('/edit/%s' % self.path) return "" def render_PUT(self, request): "accept a PUT request to set the contents of this page" if not self.wikiData.hasPage(self.path): request.setResponseCode(http.CREATED, "Created new page") else: request.setResponseCode(http.OK, "Modified existing page") request.content.seek(0) self.wikiData.setPage(self.path, request.content.read( )) return "" def render_DELETE(self, request): "accept a DELETE request to remove this page" if self.wikiData.hasPage(self.path): self.wikiData.deletePage(self.path) return "Deleted." else: request.setResponseCode(http.NOT_FOUND) return "No such page." class IndexPage(resource.Resource): def _ _init_ _(self, wikiData): self.wikiData = wikiData resource.Resource._ _init_ _(self) def render(self, request): pages = self.wikiData.listPages( ) pages.sort( ) links = ["
- <a href="%s">%s</a>
- " % (p, p) for p in pages] return """
Run wiki.py with a single argument, the name of a file to use for data storage:
$ python wiki.py wiki.dat
The script starts up a web server on port 8082. Go to http://localhost:8082/, and you'll be able to view and edit pages in the Wiki. WikiWords will automatically be converted into links. Figure 5-1 shows the default Wiki home page. Figure 5-2 shows the Edit Page form.
Figure 5-1. Browsing the Wiki
Figure 5-2. Editing a Wiki page
You're not limited to using a web browser to update the Wiki, however. For example, you can use command-line tools like wget or curl to create pages by posting data to the resource /save at http://localhost:8082/save:
$ wget -q --post-data="path=WgetTest&data=Hello%20from%20wget" --output-document=- http://localhost:8082/save $ curl -d "path=CurlTest&data=Hello%20from%20curl" http://localhost:8082/save
Example 5-2 shows how to use a Twisted HTTP client to create, modify, and delete pages in this Wiki.
5.1.2. How Does That Work?
The wiki.py application in Example 5-1 uses a class called WikiData to manage a dictionary of Wiki pages. Then it defines a series of resources, makes them available through a twisted.web.server.Server, and starts the server on port 8082. All the resource classes inherit from twisted.web.resource.Resource (for the basics on the resource.Resource class, see Chapter 4).
The WikiPage class represents a single page in the Wiki. In keeping with the REST architectural style, the class provides a two-way mechanism for transferring a representation of the page. When a client sends a GET request, WikiPage will return a rendered HTML version of the page, suitable for viewing in the browser. To update the page, a client can send an HTTP PUT request containing the new version of the page. PUT is an oft-neglected part of HTTP that allows a client to send the server data that should be used to create or replace the resource at a given URI. WikiPage also supports the HTTP DELETE method, which will completely remove the page. The separate functions render_GET, render_PUT, and render_DELETE take advantage of a built-in feature of the resource.Resource class: if these methods are defined separately, then resource.Resource will use a method matching the HTTP method used in the incoming request (and return an HTTP 405 "Method not Allowed" response if none is defined).
Unfortunately, most modern browsers don't provide a nice way to use PUT, so the server needs to offer an alternative means to create and update pages. The PageSaverResource class accepts a POST request with two arguments: the path of a page, and the page data. It will set the contents of the page at the given path, and then redirect the client so he can view it. PageSaverResource is purely a service, meant to accept input in the form of a POST request. Still, it responds to a GET request with the closest thing to a representation of itself: documentation describing how the service should be used.
The EditPage class generates an HTML form for posting to the PageSaverResource. EditPage objects are dynamically created in the getChild method of EditPageFactory, so you can get an edit form for any page by requesting the resource /edit/PageName. In traditional script-based web programming, this same thing might have been done using a URI with an appended query, something like edit.php?path=PageName. Twisted's web server gives you more control, so you can design a URI that looks nicer and isn't so closely tied to the underlying technology.
For convenience, WikiPage will respond to a request for a page that doesn't exist by returning a 302 response that redirects the client to the URI of the corresponding EditPage. This response makes it possible to link to pages in the Wiki whether or not they currently exist; if they don't, the server will do the right thing and prompt the client to create the page.
A final class, IndexPage, renders an HTML list of all the pages in the Wiki. Working together, these classes make up a web application that follows the principles of REST. It invites the client to view the application as a set of resources whose state can be retrieved or set through HTTP requests. These resources provide an HTML interface that can be used by a web browser, but they simultaneously define an API for web services. The next example shows how you can write a Twisted client to work with these resources.
Категории