Merge lp:~leonardr/lazr.restful/multiversion-named-operation into lp:lazr.restful
- multiversion-named-operation
- Merge into trunk
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 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Francis J. Lacoste (community) | code | Approve | |
Review via email: mp+16989@code.launchpad.net |
Commit message
Description of the change
Leonard Richardson (leonardr) wrote : | # |
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/
> ...
> 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_
> + ... '/1.0/contacts',
> + ... environ=
> + >>> operation = request_
> + >>> result = simplejson.
> + >>> [contact['name'] for contact in result['entries']]
> + ['Cleo Python']
> +
> + >>> request_10 = create_
> + ... '/1.0/contacts',
> + ... environ=
> + >>> operation = request_
> + >>> result = simplejson.
> + >>> [contact[
> + ['111-2121']
> +
> +
> Dev
You might want to show that using the old name would return a 404.
--
Francis J. Lacoste
<email address hidden>
Preview Diff
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 |
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.