Merge lp:~leonardr/lazr.restful/multiversion-named-operation into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Approved by: Francis J. Lacoste
Approved revision: 131
Merged at revision: not available
Proposed branch: lp:~leonardr/lazr.restful/multiversion-named-operation
Merge into: lp:lazr.restful
Prerequisite: lp:~leonardr/lazr.restful/multiversion-collection
Diff against target: 355 lines (+209/-70)
1 file modified
src/lazr/restful/docs/multiversion.txt (+209/-70)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/multiversion-named-operation
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) code Approve
Review via email: mp+16989@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

This branch shows how to support different named operations in different versions of the web service. I didn't have to change any code because named operations are already looked up based on a combination of (data model object, request). Registering different operations for different request types works automatically.

So this branch just adds some illustrative tests to multiversion.txt.

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

On January 7, 2010, Leonard Richardson wrote:
> Leonard Richardson has proposed merging
> lp:~leonardr/lazr.restful/multiversion-named-operation into
> lp:lazr.restful with lp:~leonardr/lazr.restful/multiversion-collection as
> a prerequisite.
>
> Requested reviews:
> LAZR Developers (lazr-developers)
>
>
> This branch shows how to support different named operations in different
> versions of the web service. I didn't have to change any code because
> named operations are already looked up based on a combination of (data
> model object, request). Registering different operations for different
> request types works automatically.
>
> So this branch just adds some illustrative tests to multiversion.txt.
>

Hi Leonard,

I only have one comment on that branch.

  review approve code
  status approved

> === modified file 'src/lazr/restful/docs/multiversion.txt'

> ...
> NotFound: Object: <Contact object...>, name: u'fax'
>
> +We can invoke a named operation. Note that the name of the operation
> +is now 'find' (it was 'findContacts' in 'beta').
> +
> + >>> request_beta = create_web_service_request(
> + ... '/1.0/contacts',
> + ... environ={'QUERY_STRING' : 'ws.op=find&string=Cleo'})
> + >>> operation = request_beta.traverse(None)
> + >>> result = simplejson.loads(operation())
> + >>> [contact['name'] for contact in result['entries']]
> + ['Cleo Python']
> +
> + >>> request_10 = create_web_service_request(
> + ... '/1.0/contacts',
> + ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})
> + >>> operation = request_10.traverse(None)
> + >>> result = simplejson.loads(operation())
> + >>> [contact['fax_number'] for contact in result['entries']]
> + ['111-2121']
> +
> +
> Dev

You might want to show that using the old name would return a 404.

--
Francis J. Lacoste
<email address hidden>

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/docs/multiversion.txt'
2--- src/lazr/restful/docs/multiversion.txt 2010-01-07 23:42:13 +0000
3+++ src/lazr/restful/docs/multiversion.txt 2010-01-07 23:42:14 +0000
4@@ -126,9 +126,9 @@
5 ... phone = TextLine(title=u"Phone number", required=True)
6 ... fax = TextLine(title=u"Fax number", required=False)
7
8-Here's the interface for the 'set' object that manages the contacts.
9+Here's an interface for the 'set' object that manages the
10+contacts.
11
12- >>> from zope.interface import implements
13 >>> from lazr.restful.interfaces import ITraverseWithGet
14 >>> class IContactSet(ITestDataObject, ITraverseWithGet):
15 ... def getAllContacts():
16@@ -136,10 +136,14 @@
17 ...
18 ... def get(request, name):
19 ... "Retrieve a single contact by name."
20+ ...
21+ ... def findContacts(self, string, search_fax):
22+ ... """Find contacts by name, phone number, or fax number."""
23
24 Here's a simple implementation of IContact.
25
26 >>> from urllib import quote
27+ >>> from zope.interface import implements
28 >>> from lazr.restful.security import protect_schema
29 >>> class Contact:
30 ... implements(IContact)
31@@ -176,6 +180,12 @@
32 ... def getAllContacts(self):
33 ... return self.contacts
34 ...
35+ ... def findContacts(self, string, search_fax=True):
36+ ... return [contact for contact in self.contacts
37+ ... if (string in contact.name
38+ ... or string in contact.phone
39+ ... or (search_fax and string in contact.fax))]
40+ ...
41 ... def __parent__(self, request):
42 ... return getUtility(IServiceRootResource, name=request.version)
43 ...
44@@ -377,9 +387,13 @@
45 Implementing the collection resource
46 ====================================
47
48-The contact collection itself doesn't change between versions (though
49-it could). We'll define it once and register it for every version of
50-the web service.
51+The set of contacts publishes a slightly different named operation in
52+every version of the web service, so in a little bit we'll be
53+implementing three different versions of the same named operation. But
54+the contact set itself doesn't change between versions (although it
55+could). So it's sufficient to implement one ICollection implementation
56+and register it as the implementation for every version of the web
57+service.
58
59 >>> from lazr.restful import Collection
60 >>> from lazr.restful.interfaces import ICollection
61@@ -394,19 +408,133 @@
62 ... """Find all the contacts."""
63 ... return self.context.getAllContacts()
64
65-Let's make sure ContactCollection implements ICollection.
66+Let's make sure it implements ICollection.
67
68 >>> from zope.interface.verify import verifyObject
69 >>> contact_set = ContactSet()
70 >>> verifyObject(ICollection, ContactCollection(contact_set, None))
71 True
72
73-Once we register ContactCollection as the ICollection implementation,
74-we can adapt the ContactSet object to a web service ICollection.
75-
76+Register it as the ICollection adapter for IContactSet. We use a
77+generic request interface (IWebServiceClientRequest) rather than a
78+specific one like IWebServiceRequestBeta, so that the same
79+implementation will be used for every version of the web service.
80+
81+ >>> sm.registerAdapter(
82+ ... ContactCollection, [IContactSet, IWebServiceClientRequest],
83+ ... provided=ICollection)
84+
85+Make sure the functionality works properly.
86+
87+ >>> collection = getMultiAdapter(
88+ ... (contact_set, request_beta), ICollection)
89+ >>> len(collection.find())
90+ 2
91+
92+Implementing the named operations
93+---------------------------------
94+
95+All three versions of the web service publish a named operation for
96+searching for contacts, but they publish it in slightly different
97+ways. In 'beta' it publishes a named operation called 'findContacts',
98+which does a search based on name, phone number, and fax number. In
99+'1.0' it publishes the same operation, but the name is
100+'find'. In 'dev' the contact set publishes 'find',
101+but the functionality is changed to search only the name and phone
102+number.
103+
104+Here's the named operation as implemented in versions 'beta' and '1.0'.
105+
106+ >>> from lazr.restful import ResourceGETOperation
107+ >>> from lazr.restful.fields import CollectionField, Reference
108+ >>> from lazr.restful.interfaces import IResourceGETOperation
109+ >>> class FindContactsOperationBase(ResourceGETOperation):
110+ ... """An operation that searches for contacts."""
111+ ... implements(IResourceGETOperation)
112+ ...
113+ ... params = [ TextLine(__name__='string') ]
114+ ... return_type = CollectionField(value_type=Reference(schema=IContact))
115+ ...
116+ ... def call(self, string):
117+ ... try:
118+ ... return self.context.findContacts(string)
119+ ... except ValueError, e:
120+ ... self.request.response.setStatus(400)
121+ ... return str(e)
122+
123+This operation is registered as the "findContacts" operation in the
124+'beta' service, and the 'find' operation in the '1.0' service.
125+
126+ >>> sm.registerAdapter(
127+ ... FindContactsOperationBase, [IContactSet, IWebServiceRequestBeta],
128+ ... provided=IResourceGETOperation, name="findContacts")
129+
130+ >>> sm.registerAdapter(
131+ ... FindContactsOperationBase, [IContactSet, IWebServiceRequest10],
132+ ... provided=IResourceGETOperation, name="find")
133+
134+Here's the slightly different named operation as implemented in
135+version 'dev'.
136+
137+ >>> class FindContactsOperationNoFax(FindContactsOperationBase):
138+ ... """An operation that searches for contacts."""
139+ ...
140+ ... def call(self, string):
141+ ... try:
142+ ... return self.context.findContacts(string, False)
143+ ... except ValueError, e:
144+ ... self.request.response.setStatus(400)
145+ ... return str(e)
146+
147+ >>> sm.registerAdapter(
148+ ... FindContactsOperationNoFax, [IContactSet, IWebServiceRequestDev],
149+ ... provided=IResourceGETOperation, name="find")
150+
151+The service root resource
152+=========================
153+
154+To make things more interesting we'll define two distinct service
155+roots. The 'beta' web service will publish the contact set as
156+'contact_list', and subsequent versions will publish it as 'contacts'.
157+
158+ >>> from lazr.restful.simple import RootResource
159+ >>> from zope.traversing.browser.interfaces import IAbsoluteURL
160+
161+ >>> class BetaServiceRootResource(RootResource):
162+ ... implements(IAbsoluteURL)
163+ ...
164+ ... top_level_collections = {
165+ ... 'contact_list': (IContact, ContactSet()) }
166+
167+ >>> class PostBetaServiceRootResource(RootResource):
168+ ... implements(IAbsoluteURL)
169+ ...
170+ ... top_level_collections = {
171+ ... 'contacts': (IContact, ContactSet()) }
172+
173+ >>> for version, cls in (('beta', BetaServiceRootResource),
174+ ... ('1.0', PostBetaServiceRootResource),
175+ ... ('dev', PostBetaServiceRootResource)):
176+ ... app = cls()
177+ ... sm.registerUtility(app, IServiceRootResource, name=version)
178+
179+ >>> beta_app = getUtility(IServiceRootResource, 'beta')
180+ >>> dev_app = getUtility(IServiceRootResource, 'dev')
181+
182+ >>> beta_app.top_level_names
183+ ['contact_list']
184+
185+ >>> dev_app.top_level_names
186+ ['contacts']
187+
188+Both classes will use the default lazr.restful code to generate their
189+URLs.
190+
191+ >>> from zope.traversing.browser import absoluteURL
192+ >>> from lazr.restful.simple import RootResourceAbsoluteURL
193+ >>> for cls in (BetaServiceRootResource, PostBetaServiceRootResource):
194+ ... sm.registerAdapter(
195 <<<<<<< TREE
196- >>> for version in ['beta', 'dev', '1.0']:
197- ... sm.registerAdapter(
198 ... ContactCollection, provided=ICollection, name=version)
199
200 >>> dev_collection = getAdapter(contact_set, ICollection, name="dev")
201@@ -516,65 +644,6 @@
202 http://api.multiversion.dev/dev/
203
204 =======
205- >>> for request, ignore in versions:
206- ... sm.registerAdapter(
207- ... ContactCollection, [IContactSet, request],
208- ... provided=ICollection)
209-
210- >>> beta_collection = getMultiAdapter(
211- ... (contact_set, request_beta), ICollection)
212- >>> len(beta_collection.find())
213- 2
214-
215- >>> dev_collection = getMultiAdapter(
216- ... (contact_set, request_dev), ICollection)
217- >>> len(dev_collection.find())
218- 2
219-
220-The service root resource
221-=========================
222-
223-To make things more interesting we'll define two distinct service
224-roots. The 'beta' web service will publish the contact set as
225-'contact_list', and subsequent versions will publish it as 'contacts'.
226-
227- >>> from lazr.restful.simple import RootResource
228- >>> from zope.traversing.browser.interfaces import IAbsoluteURL
229-
230- >>> class BetaServiceRootResource(RootResource):
231- ... implements(IAbsoluteURL)
232- ...
233- ... top_level_collections = {
234- ... 'contact_list': (IContact, ContactSet()) }
235-
236- >>> class PostBetaServiceRootResource(RootResource):
237- ... implements(IAbsoluteURL)
238- ...
239- ... top_level_collections = {
240- ... 'contacts': (IContact, ContactSet()) }
241-
242- >>> for version, cls in (('beta', BetaServiceRootResource),
243- ... ('1.0', PostBetaServiceRootResource),
244- ... ('dev', PostBetaServiceRootResource)):
245- ... app = cls()
246- ... sm.registerUtility(app, IServiceRootResource, name=version)
247-
248- >>> beta_app = getUtility(IServiceRootResource, 'beta')
249- >>> dev_app = getUtility(IServiceRootResource, 'dev')
250-
251- >>> beta_app.top_level_names
252- ['contact_list']
253-
254- >>> dev_app.top_level_names
255- ['contacts']
256-
257-Both classes will use the default lazr.restful code to generate their
258-URLs.
259-
260- >>> from zope.traversing.browser import absoluteURL
261- >>> from lazr.restful.simple import RootResourceAbsoluteURL
262- >>> for cls in (BetaServiceRootResource, PostBetaServiceRootResource):
263- ... sm.registerAdapter(
264 ... RootResourceAbsoluteURL, [cls, IBrowserRequest])
265
266 >>> beta_request = create_web_service_request('/beta/')
267@@ -671,6 +740,26 @@
268 >>> print simplejson.loads(field())
269 111-2121
270
271+We can invoke a named operation.
272+
273+ >>> import simplejson
274+ >>> request_beta = create_web_service_request(
275+ ... '/beta/contact_list',
276+ ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=Cleo'})
277+ >>> operation = request_beta.traverse(None)
278+ >>> result = simplejson.loads(operation())
279+ >>> [contact['name'] for contact in result['entries']]
280+ ['Cleo Python']
281+
282+ >>> request_beta = create_web_service_request(
283+ ... '/beta/contact_list',
284+ ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=111'})
285+
286+ >>> operation = request_beta.traverse(None)
287+ >>> result = simplejson.loads(operation())
288+ >>> [contact['fax'] for contact in result['entries']]
289+ ['111-2121']
290+
291 1.0
292 ---
293
294@@ -750,6 +839,34 @@
295 ...
296 NotFound: Object: <Contact object...>, name: u'fax'
297
298+We can invoke a named operation. Note that the name of the operation
299+is now 'find' (it was 'findContacts' in 'beta').
300+
301+ >>> request_10 = create_web_service_request(
302+ ... '/1.0/contacts',
303+ ... environ={'QUERY_STRING' : 'ws.op=find&string=Cleo'})
304+ >>> operation = request_10.traverse(None)
305+ >>> result = simplejson.loads(operation())
306+ >>> [contact['name'] for contact in result['entries']]
307+ ['Cleo Python']
308+
309+ >>> request_10 = create_web_service_request(
310+ ... '/1.0/contacts',
311+ ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})
312+ >>> operation = request_10.traverse(None)
313+ >>> result = simplejson.loads(operation())
314+ >>> [contact['fax_number'] for contact in result['entries']]
315+ ['111-2121']
316+
317+Attempting to invoke the operation using its 'beta' name won't work.
318+
319+ >>> request_10 = create_web_service_request(
320+ ... '/1.0/contacts',
321+ ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=Cleo'})
322+ >>> operation = request_10.traverse(None)
323+ >>> print operation()
324+ No such operation: findContacts
325+
326 Dev
327 ---
328
329@@ -822,4 +939,26 @@
330 Traceback (most recent call last):
331 ...
332 NotFound: Object: <Contact object...>, name: u'fax_number'
333+
334+We can invoke a named operation.
335+
336+ >>> request_dev = create_web_service_request(
337+ ... '/dev/contacts',
338+ ... environ={'QUERY_STRING' : 'ws.op=find&string=Cleo'})
339+ >>> operation = request_dev.traverse(None)
340+ >>> result = simplejson.loads(operation())
341+ >>> [contact['name'] for contact in result['entries']]
342+ ['Cleo Python']
343+
344+Note that a search for Cleo's fax number no longer finds anything,
345+because the named operation published as 'find' in the 'dev' web
346+service doesn't search the fax field.
347+
348+ >>> request_dev = create_web_service_request(
349+ ... '/dev/contacts',
350+ ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})
351+ >>> operation = request_dev.traverse(None)
352+ >>> result = simplejson.loads(operation())
353+ >>> result['total_size']
354+ 0
355 >>>>>>> MERGE-SOURCE

Subscribers

People subscribed via source and target branches