Merge lp:~leonardr/lazr.restful/double-your-enjoyment into lp:lazr.restful
- double-your-enjoyment
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Edwin Grubbs |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp:~leonardr/lazr.restful/double-your-enjoyment |
Merge into: | lp:lazr.restful |
Diff against target: |
539 lines (+330/-55) 2 files modified
src/lazr/restful/docs/multiversion.txt (+292/-0) src/lazr/restful/docs/webservice.txt (+38/-55) |
To merge this branch: | bzr merge lp:~leonardr/lazr.restful/double-your-enjoyment |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Edwin Grubbs (community) | code | Approve | |
Review via email: mp+14967@code.launchpad.net |
Commit message
Description of the change
Leonard Richardson (leonardr) wrote : | # |
- 97. By Leonard Richardson
-
Fixed the prose.
Edwin Grubbs (edwin-grubbs) wrote : | # |
Hi Leonard,
This branch looks good. I have some very minor comments below.
merge-approved
-Edwin
>=== added file 'src/lazr/
>+ >>> from lazr.restful.simple import TraverseWithGet
>+ >>> from zope.publisher.
>+ >>> class ContactSet(
>+ ... implements(
>+ ... path = "contact"
Please fix the path variable as discussed on IRC.
>+ ...
>+ ... def __init__(self):
>+ ... self.contacts = CONTACTS
>+ ...
>+ ... def get(self, name):
>+ ... contacts = [contact for contacts in self.contacts
>+ ... if pair.name == name]
>+ ... if len(contacts) == 1:
>+ ... return contacts[0]
>+ ... return None
>+ >>> sm.registerAdap
Line too long.
>=== modified file 'src/lazr/
>@@ -417,16 +406,15 @@
> ... def getAllAuthors(
> ... return self.authors
> ...
>- ... def get(self, name):
>+ ... def get(self, request, name):
> ... match = [p for p in self.authors if p.name == name]
> ... if len(match) > 0:
> ... return match[0]
> ... return None
>
>+ >>> sm.registerAdap
Line too long.
Preview Diff
1 | === added file 'src/lazr/restful/docs/multiversion.txt' |
2 | --- src/lazr/restful/docs/multiversion.txt 1970-01-01 00:00:00 +0000 |
3 | +++ src/lazr/restful/docs/multiversion.txt 2009-11-18 16:46:09 +0000 |
4 | @@ -0,0 +1,292 @@ |
5 | +Publishing multiple versions of a web service |
6 | +********************************************* |
7 | + |
8 | +A single data model can yield many different lazr.restful web |
9 | +services. Typically these different services represent successive |
10 | +versions of a single web service, improved over time. |
11 | + |
12 | +Example model objects |
13 | +===================== |
14 | + |
15 | +First let's define the data model. The model in webservice.txt is |
16 | +pretty complicated; this model will be just complicated enough to |
17 | +illustrate how to publish multiple versions of a web service. |
18 | + |
19 | + # All classes defined in this test are new-style classes. |
20 | + >>> __metaclass__ = type |
21 | + |
22 | + >>> from zope.interface import Interface, Attribute |
23 | + >>> from zope.schema import Bool, Bytes, Int, Text, TextLine, Object |
24 | + |
25 | + >>> class ITestDataObject(Interface): |
26 | + ... """A marker interface for data objects.""" |
27 | + ... path = Attribute("The path portion of this object's URL. " |
28 | + ... "Defined here for simplicity of testing.") |
29 | + |
30 | + >>> class IContact(ITestDataObject): |
31 | + ... name = TextLine(title=u"Name", required=True) |
32 | + ... phone = TextLine(title=u"Phone number", required=True) |
33 | + ... fax = TextLine(title=u"Fax number", required=False) |
34 | + |
35 | +Here's the interface for the 'set' object that manages the contacts. |
36 | + |
37 | + >>> from lazr.restful.interfaces import ITraverseWithGet |
38 | + >>> class IContactSet(ITestDataObject, ITraverseWithGet): |
39 | + ... def getAll(self): |
40 | + ... "Get all contacts." |
41 | + ... |
42 | + ... def get(self, name): |
43 | + ... "Retrieve a single contact by name." |
44 | + |
45 | +Before we can define any classes, a bit of web service setup. Let's |
46 | +make all component lookups use the global site manager. |
47 | + |
48 | + >>> from zope.component import getSiteManager |
49 | + >>> sm = getSiteManager() |
50 | + |
51 | + >>> from zope.component import adapter |
52 | + >>> from zope.component.interfaces import IComponentLookup |
53 | + >>> from zope.interface import implementer, Interface |
54 | + >>> @implementer(IComponentLookup) |
55 | + ... @adapter(Interface) |
56 | + ... def everything_uses_the_global_site_manager(context): |
57 | + ... return sm |
58 | + >>> sm.registerAdapter(everything_uses_the_global_site_manager) |
59 | + |
60 | +Here's a simple implementation of IContact. |
61 | + |
62 | + >>> from urllib import quote |
63 | + >>> from zope.interface import implements |
64 | + >>> from lazr.restful.security import protect_schema |
65 | + >>> class Contact: |
66 | + ... implements(IContact) |
67 | + ... def __init__(self, name, phone, fax): |
68 | + ... self.name = name |
69 | + ... self.phone = phone |
70 | + ... self.fax = fax |
71 | + ... |
72 | + ... @property |
73 | + ... def path(self): |
74 | + ... return 'contacts/' + quote(self.name) |
75 | + >>> protect_schema(Contact, IContact) |
76 | + |
77 | +Here's a simple ContactSet with a predefined list of contacts. |
78 | + |
79 | + >>> from lazr.restful.simple import TraverseWithGet |
80 | + >>> from zope.publisher.interfaces.browser import IBrowserRequest |
81 | + >>> class ContactSet(TraverseWithGet): |
82 | + ... implements(IContactSet) |
83 | + ... path = "contact" |
84 | + ... |
85 | + ... def __init__(self): |
86 | + ... self.contacts = CONTACTS |
87 | + ... |
88 | + ... def get(self, name): |
89 | + ... contacts = [contact for contacts in self.contacts |
90 | + ... if pair.name == name] |
91 | + ... if len(contacts) == 1: |
92 | + ... return contacts[0] |
93 | + ... return None |
94 | + >>> sm.registerAdapter(TraverseWithGet, [ITestDataObject, IBrowserRequest]) |
95 | + |
96 | +Here are the "model objects" themselves: |
97 | + |
98 | + >>> C1 = Contact("Cleo Python", "555-1212", "111-2121") |
99 | + >>> C2 = Contact("Oliver Bluth", "10-1000000", "22-2222222") |
100 | + >>> CONTACTS = [C1, C2] |
101 | + |
102 | +Web service infrastructure initialization |
103 | +========================================= |
104 | + |
105 | +The lazr.restful package contains a set of default adapters and |
106 | +definitions to implement the web service. |
107 | + |
108 | + >>> from zope.configuration import xmlconfig |
109 | + >>> zcmlcontext = xmlconfig.string(""" |
110 | + ... <configure xmlns="http://namespaces.zope.org/zope"> |
111 | + ... <include package="lazr.restful" file="basic-site.zcml"/> |
112 | + ... <utility |
113 | + ... factory="lazr.restful.example.base.filemanager.FileManager" /> |
114 | + ... </configure> |
115 | + ... """) |
116 | + |
117 | +A IWebServiceConfiguration utility is also expected to be defined which |
118 | +defines common configuration option for the webservice. |
119 | + |
120 | + >>> from lazr.restful import directives |
121 | + >>> from lazr.restful.interfaces import IWebServiceConfiguration |
122 | + >>> from lazr.restful.simple import BaseWebServiceConfiguration |
123 | + >>> from lazr.restful.testing.webservice import WebServiceTestPublication |
124 | + |
125 | + >>> class WebServiceConfiguration(BaseWebServiceConfiguration): |
126 | + ... use_https = False |
127 | + ... active_versions = ['beta', '1.0'] |
128 | + ... latest_version_uri_prefix = 'dev' |
129 | + ... code_revision = 'test' |
130 | + ... max_batch_size = 100 |
131 | + ... directives.publication_class(WebServiceTestPublication) |
132 | + |
133 | + >>> from grokcore.component.testing import grok_component |
134 | + >>> ignore = grok_component( |
135 | + ... 'WebServiceConfiguration', WebServiceConfiguration) |
136 | + |
137 | + >>> from zope.component import getUtility |
138 | + >>> webservice_configuration = getUtility(IWebServiceConfiguration) |
139 | + |
140 | +Defining the web service data model |
141 | +=================================== |
142 | + |
143 | +We've defined an underlying data model (IContact), and now we're going |
144 | +to define the evolution of a web service through three versions, by |
145 | +defining three derivative data models. In a real application, these |
146 | +IEntry subclasses would be generated from lazr.restful decorators |
147 | +present in IContact but for testing purposes we're going to just |
148 | +define the three IEntry subclasses manually. |
149 | + |
150 | +The "beta" version of the web service publishes the IContact interface |
151 | +exactly as it is defined. |
152 | + |
153 | + >>> from lazr.restful.interfaces import IEntry |
154 | + >>> class IContactEntry(IEntry): |
155 | + ... """Marker for a contact published through the web service.""" |
156 | + |
157 | + >>> from zope.interface import taggedValue |
158 | + >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME |
159 | + >>> class IContactEntryBeta(IContactEntry, IContact): |
160 | + ... """The part of an author we expose through the web service.""" |
161 | + ... taggedValue(LAZR_WEBSERVICE_NAME, |
162 | + ... dict(singular="contact", plural="contacts")) |
163 | + |
164 | +The "1.0" version publishes the IContact interface as is, but renames |
165 | +two of the fields. |
166 | + |
167 | + >>> class IContactEntry10(IContactEntry): |
168 | + ... name = TextLine(title=u"Name", required=True) |
169 | + ... phone_number = TextLine(title=u"Phone number", required=True) |
170 | + ... fax_number = TextLine(title=u"Fax number", required=False) |
171 | + ... taggedValue(LAZR_WEBSERVICE_NAME, |
172 | + ... dict(singular="contact", plural="contacts")) |
173 | + |
174 | +The "dev" version drops the "fax_number" field because fax machines |
175 | +are obsolete. |
176 | + |
177 | + >>> class IContactEntryDev(IContactEntry): |
178 | + ... name = TextLine(title=u"Name", required=True) |
179 | + ... phone_number = TextLine(title=u"Phone number", required=True) |
180 | + ... taggedValue(LAZR_WEBSERVICE_NAME, |
181 | + ... dict(singular="contact", plural="contacts")) |
182 | + |
183 | +Implementing the resources |
184 | +========================== |
185 | + |
186 | +The Contact class defined above implements the IContact interface, but |
187 | +IContact just describes the data model, not any particular web |
188 | +service. The IContactEntry subclasses above -- IContactEntryBeta, |
189 | +IContactEntry10, IContactEntryDev -- describe the three versions of |
190 | +the web service. Each of these interfaces must be implemented by a |
191 | +subclass of Entry. |
192 | + |
193 | +In a real application, the Entry subclasses would be generated from |
194 | +lazr.restful decorators present in IContact (just like the IEntry |
195 | +subclasses), but for testing purposes we're going to define the three |
196 | +Entry subclasses manually. |
197 | + |
198 | + >>> from zope.component import adapts |
199 | + >>> from zope.interface import implements |
200 | + >>> from lazr.delegates import delegates |
201 | + >>> from lazr.restful import Entry |
202 | + |
203 | + >>> class ContactEntryBeta(Entry): |
204 | + ... """A contact, as exposed through the 'beta' web service.""" |
205 | + ... adapts(IContact) |
206 | + ... implements(IContactEntryBeta) |
207 | + ... delegates(IContactEntryBeta) |
208 | + ... schema = IContactEntryBeta |
209 | + >>> sm.registerAdapter( |
210 | + ... ContactEntryBeta, provided=IContactEntry, name="beta") |
211 | + |
212 | +By wrapping one of our predefined Contacts in a ContactEntryBeta |
213 | +object, we can verify that it implements IContactEntryBeta and |
214 | +IContactEntry. |
215 | + |
216 | + >>> entry = ContactEntryBeta(C1) |
217 | + >>> IContactEntry.validateInvariants(entry) |
218 | + >>> IContactEntryBeta.validateInvariants(entry) |
219 | + |
220 | +Here's the implemenation of IContactEntry10, which defines Python |
221 | +properties to implement the different field names. |
222 | + |
223 | + >>> class ContactEntry10(Entry): |
224 | + ... adapts(IContact) |
225 | + ... implements(IContactEntry10) |
226 | + ... schema = IContactEntry10 |
227 | + ... |
228 | + ... def __init__(self, contact): |
229 | + ... self.contact = contact |
230 | + ... |
231 | + ... @property |
232 | + ... def phone_number(self): |
233 | + ... return self.contact.phone |
234 | + ... |
235 | + ... @property |
236 | + ... def fax_number(self): |
237 | + ... return self.contact.fax |
238 | + >>> sm.registerAdapter( |
239 | + ... ContactEntry10, provided=IContactEntry, name="1.0") |
240 | + |
241 | + >>> entry = ContactEntry10(C1) |
242 | + >>> IContactEntry.validateInvariants(entry) |
243 | + >>> IContactEntry10.validateInvariants(entry) |
244 | + |
245 | +Finally, here's the implementation of IContactEntry for the "dev" version of |
246 | +the web service. |
247 | + |
248 | + >>> class ContactEntryDev(Entry): |
249 | + ... adapts(IContact) |
250 | + ... implements(IContactEntryDev) |
251 | + ... schema = IContactEntryDev |
252 | + ... |
253 | + ... def __init__(self, contact): |
254 | + ... self.contact = contact |
255 | + ... |
256 | + ... @property |
257 | + ... def phone_number(self): |
258 | + ... return self.contact.phone |
259 | + >>> sm.registerAdapter( |
260 | + ... ContactEntryDev, provided=IContactEntry, name="dev") |
261 | + |
262 | + >>> entry = ContactEntryDev(C1) |
263 | + >>> IContactEntry.validateInvariants(entry) |
264 | + >>> IContactEntryDev.validateInvariants(entry) |
265 | + |
266 | +Looking up the appropriate implementation |
267 | +========================================= |
268 | + |
269 | +Because there is no single IEntry implementation for Contact objects, |
270 | +you can't just adapt Contact to IEntry. |
271 | + |
272 | + >>> from zope.component import getAdapter |
273 | + |
274 | + >>> getAdapter(C1, IEntry) |
275 | + Traceback (most recent call last): |
276 | + ... |
277 | + ComponentLookupError: ... |
278 | + |
279 | +When adapting Contact to IEntry you must specify a version number as |
280 | +the name of the adapter. The object you get back will implement the |
281 | +appropriate version of the web service. |
282 | + |
283 | + >>> beta_entry = getAdapter(C1, IEntry, name="beta") |
284 | + >>> print beta_entry.fax |
285 | + 111-2121 |
286 | + |
287 | + >>> one_oh_entry = getAdapter(C1, IEntry, name="1.0") |
288 | + >>> print one_oh_entry.fax_number |
289 | + 111-2121 |
290 | + |
291 | + >>> dev_entry = getAdapter(C1, IEntry, name="dev") |
292 | + >>> print dev_entry.fax |
293 | + Traceback (most recent call last): |
294 | + ... |
295 | + AttributeError: 'ContactEntryDev' object has no attribute 'fax' |
296 | + |
297 | |
298 | === modified file 'src/lazr/restful/docs/webservice.txt' |
299 | --- src/lazr/restful/docs/webservice.txt 2009-11-12 19:08:10 +0000 |
300 | +++ src/lazr/restful/docs/webservice.txt 2009-11-18 16:46:09 +0000 |
301 | @@ -74,28 +74,32 @@ |
302 | Here's the interface for the 'set' objects that manage the authors, |
303 | cookbooks, and dishes. The inconsistent naming is intentional. |
304 | |
305 | - >>> class IAuthorSet(ITestDataObject): |
306 | + >>> from lazr.restful.interfaces import ITraverseWithGet |
307 | + >>> class ITestDataSet(ITestDataObject, ITraverseWithGet): |
308 | + ... """A marker interface.""" |
309 | + |
310 | + >>> class IAuthorSet(ITestDataSet): |
311 | ... def getAllAuthors(self): |
312 | ... "Get all authors." |
313 | ... |
314 | - ... def get(self, name): |
315 | + ... def get(self, request, name): |
316 | ... "Retrieve a single author by name." |
317 | |
318 | - >>> class ICookbookSet(ITestDataObject): |
319 | + >>> class ICookbookSet(ITestDataSet): |
320 | ... def getAll(self): |
321 | ... "Get all cookbooks." |
322 | ... |
323 | - ... def get(self, name): |
324 | + ... def get(self, request, name): |
325 | ... "Retrieve a single cookbook by name." |
326 | ... |
327 | ... def findRecipes(self, name): |
328 | ... "Find recipes with a given name, across cookbooks." |
329 | |
330 | - >>> class IDishSet(ITestDataObject): |
331 | + >>> class IDishSet(ITestDataSet): |
332 | ... def getAll(self): |
333 | ... "Get all dishes." |
334 | ... |
335 | - ... def get(self, name): |
336 | + ... def get(self, request, name): |
337 | ... "Retrieve a single dish by name." |
338 | |
339 | |
340 | @@ -143,23 +147,6 @@ |
341 | ... BaseAbsoluteURL, [ITestDataObject, IBrowserRequest], |
342 | ... IAbsoluteURL) |
343 | |
344 | - >>> from urllib import unquote |
345 | - >>> class Traversable: |
346 | - ... """A default IPublishTraverse that uses the get() method.""" |
347 | - ... implements(IPublishTraverse) |
348 | - ... adapts(ITestDataObject, IBrowserRequest) |
349 | - ... |
350 | - ... def __init__(self, context, request): |
351 | - ... self.context = context |
352 | - ... |
353 | - ... def publishTraverse(self, request, name): |
354 | - ... name = unquote(name) |
355 | - ... value = self.context.get(name) |
356 | - ... if value is None: |
357 | - ... raise NotFound(self, name) |
358 | - ... return value |
359 | - >>> sm.registerAdapter(Traversable) |
360 | - |
361 | >>> class Author: |
362 | ... implements(IAuthor) |
363 | ... def __init__(self, name): |
364 | @@ -219,6 +206,7 @@ |
365 | |
366 | >>> protect_schema(Cookbook, ICookbook, write_permission=CheckerPublic) |
367 | |
368 | + >>> from urllib import unquote |
369 | >>> class CookbookTraversal: |
370 | ... implements(IPublishTraverse) |
371 | ... adapts(ICookbook, IBrowserRequest) |
372 | @@ -358,7 +346,8 @@ |
373 | |
374 | Here's a simple CookbookSet with a predefined list of cookbooks. |
375 | |
376 | - >>> class CookbookSet(BaseAbsoluteURL): |
377 | + >>> from lazr.restful.simple import TraverseWithGet |
378 | + >>> class CookbookSet(BaseAbsoluteURL, TraverseWithGet): |
379 | ... implements(ICookbookSet) |
380 | ... path = 'cookbooks' |
381 | ... |
382 | @@ -367,7 +356,7 @@ |
383 | ... |
384 | ... def newCookbook(self, author_name, title, cuisine): |
385 | ... authors = AuthorSet() |
386 | - ... author = authors.get(author_name) |
387 | + ... author = authors.get(None, author_name) |
388 | ... if author is None: |
389 | ... author = authors.newAuthor(author_name) |
390 | ... cookbook = Cookbook(title, author, cuisine) |
391 | @@ -377,7 +366,7 @@ |
392 | ... def getAll(self): |
393 | ... return self.cookbooks |
394 | ... |
395 | - ... def get(self, name): |
396 | + ... def get(self, request, name): |
397 | ... match = [c for c in self.cookbooks if c.name == name] |
398 | ... if len(match) > 0: |
399 | ... return match[0] |
400 | @@ -402,7 +391,7 @@ |
401 | |
402 | Here's a simple AuthorSet with predefined authors. |
403 | |
404 | - >>> class AuthorSet(BaseAbsoluteURL): |
405 | + >>> class AuthorSet(BaseAbsoluteURL, TraverseWithGet): |
406 | ... implements(IAuthorSet) |
407 | ... path = 'authors' |
408 | ... |
409 | @@ -417,16 +406,15 @@ |
410 | ... def getAllAuthors(self): |
411 | ... return self.authors |
412 | ... |
413 | - ... def get(self, name): |
414 | + ... def get(self, request, name): |
415 | ... match = [p for p in self.authors if p.name == name] |
416 | ... if len(match) > 0: |
417 | ... return match[0] |
418 | ... return None |
419 | |
420 | + >>> sm.registerAdapter(TraverseWithGet, [ITestDataObject, IBrowserRequest]) |
421 | >>> protect_schema(AuthorSet, IAuthorSet) |
422 | >>> sm.registerUtility(AuthorSet(), IAuthorSet) |
423 | - >>> sm.registerAdapter( |
424 | - ... Traversable, [IAuthorSet, IBrowserRequest]) |
425 | |
426 | Here's a vocabulary of authors, for a field that presents a Choice |
427 | among authors. |
428 | @@ -448,7 +436,7 @@ |
429 | |
430 | Finally, a simple DishSet with predefined dishes. |
431 | |
432 | - >>> class DishSet(BaseAbsoluteURL): |
433 | + >>> class DishSet(BaseAbsoluteURL, TraverseWithGet): |
434 | ... implements(IDishSet) |
435 | ... path = 'dishes' |
436 | ... def __init__(self): |
437 | @@ -457,7 +445,7 @@ |
438 | ... def getAll(self): |
439 | ... return self.dishes |
440 | ... |
441 | - ... def get(self, name): |
442 | + ... def get(self, request, name): |
443 | ... match = [d for d in self.dishes if d.name == name] |
444 | ... if len(match) > 0: |
445 | ... return match[0] |
446 | @@ -546,27 +534,20 @@ |
447 | interface and ``IEntry``. Since ``IAuthor`` and ``IComment`` are so simple, we |
448 | can define ``IAuthorEntry`` and ``ICommentEntry`` very simply. |
449 | |
450 | - >>> from lazr.restful.interfaces import IEntry |
451 | +The only extra and unusual step we have to take is to annotate the interfaces |
452 | +with human-readable names for the objects we're exposing. |
453 | + |
454 | + >>> from zope.interface import taggedValue |
455 | + >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME |
456 | >>> class IAuthorEntry(IAuthor, IEntry): |
457 | ... """The part of an author we expose through the web service.""" |
458 | + ... taggedValue(LAZR_WEBSERVICE_NAME, dict(singular="author", |
459 | + ... plural="authors")) |
460 | |
461 | >>> class ICommentEntry(IComment, IEntry): |
462 | ... """The part of a comment we expose through the web service.""" |
463 | - |
464 | -The only extra and unusual step we have to take is to annotate the interfaces |
465 | -with human-readable names for the objects we're exposing. |
466 | - |
467 | - >>> from lazr.restful.interfaces import LAZR_WEBSERVICE_NAME |
468 | - |
469 | - # XXX leonardr 2008-07-09 bug=145746 |
470 | - # Once we're using Zope 3.4 we can call |
471 | - # zope.interface.taggedValue(LAZR_WEBSERVICE_NAME, dict(...)) |
472 | - # from within the interface declaration. |
473 | - >>> def tag_with_names(interface, singular, plural): |
474 | - ... interface.setTaggedValue( |
475 | - ... LAZR_WEBSERVICE_NAME, dict(singular=singular, plural=plural)) |
476 | - >>> tag_with_names(IAuthorEntry, 'author', 'authors') |
477 | - >>> tag_with_names(ICommentEntry, 'comment', 'comments') |
478 | + ... taggedValue(LAZR_WEBSERVICE_NAME, |
479 | + ... dict(singular="comment", plural="comments")) |
480 | |
481 | Most of the time, it doesn't work to expose to the web service the same data |
482 | model we expose internally. Usually there are fields we don't want to expose, |
483 | @@ -586,7 +567,8 @@ |
484 | >>> class IDishEntry(IEntry): |
485 | ... "The part of a dish that we expose through the web service." |
486 | ... recipes = CollectionField(value_type=Object(schema=IRecipe)) |
487 | - >>> tag_with_names(IDishEntry, 'dish', 'dishes') |
488 | + ... taggedValue(LAZR_WEBSERVICE_NAME, |
489 | + ... dict(singular="dish", plural="dishes")) |
490 | |
491 | In the following code block we define an interface that exposes the underlying |
492 | ``Recipe``'s name but not its ID. References to associated objects (like the |
493 | @@ -599,7 +581,8 @@ |
494 | ... dish = Object(schema=IDish) |
495 | ... instructions = Text(title=u"Name", required=True) |
496 | ... comments = CollectionField(value_type=Object(schema=IComment)) |
497 | - >>> tag_with_names(IRecipeEntry, 'recipe', 'recipes') |
498 | + ... taggedValue(LAZR_WEBSERVICE_NAME, |
499 | + ... dict(singular="recipe", plural="recipes")) |
500 | |
501 | >>> from lazr.restful.fields import ReferenceChoice |
502 | >>> class ICookbookEntry(IEntry): |
503 | @@ -610,7 +593,8 @@ |
504 | ... recipes = CollectionField(value_type=Object(schema=IRecipe)) |
505 | ... comments = CollectionField(value_type=Object(schema=IComment)) |
506 | ... cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.") |
507 | - >>> tag_with_names(ICookbookEntry, 'cookbook', 'cookbooks') |
508 | + ... taggedValue(LAZR_WEBSERVICE_NAME, |
509 | + ... dict(singular="cookbook", plural="cookbooks")) |
510 | |
511 | The ``author`` field is a choice between ``Author`` objects. To make sure |
512 | that the ``Author`` objects are properly marshalled to JSON, we need to |
513 | @@ -947,7 +931,7 @@ |
514 | >>> from lazr.restful import ServiceRootResource |
515 | >>> from zope.traversing.browser.interfaces import IAbsoluteURL |
516 | |
517 | - >>> class MyServiceRootResource(ServiceRootResource): |
518 | + >>> class MyServiceRootResource(ServiceRootResource, TraverseWithGet): |
519 | ... implements(IAbsoluteURL) |
520 | ... path = '' |
521 | ... |
522 | @@ -956,7 +940,7 @@ |
523 | ... 'cookbooks': CookbookSet(), |
524 | ... 'authors': AuthorSet()} |
525 | ... |
526 | - ... def get(self, name): |
527 | + ... def get(self, request, name): |
528 | ... return self.top_level_names.get(name) |
529 | |
530 | It's the responsibility of each web service to provide an implementation of |
531 | @@ -964,8 +948,7 @@ |
532 | |
533 | >>> sm.registerAdapter( |
534 | ... BaseAbsoluteURL, [MyServiceRootResource, IBrowserRequest]) |
535 | - >>> sm.registerAdapter( |
536 | - ... Traversable, [IServiceRootResource, IBrowserRequest]) |
537 | + |
538 | >>> app = MyServiceRootResource() |
539 | >>> sm.registerUtility(app, IServiceRootResource) |
540 |
This branch creates a new doctest, multiversion.txt, which defines a web service that serves three distinct services ("beta", "1.0", and "dev") from a single underlying data model. Currently there is no code that uses this data model with lazr.restful, which is good because as soon as I add that code lazr.restful is going to explode. I'm submitting this branch as a natural stopping point before I have to start tearing up lazr.restful and putting it back together.
I'm using webservice.txt as a model for multiversion.txt. This is a little tricky because webservice.txt is a very old service that reimplements many of the niceties found in lazr.restful. simple. So I'm trying to improve webservice.txt as I copy it. For instance, I removed the Traversable class from webservice.txt and replaced it with the standard TraverseWithGet class. I also got rid of an XXX that said to make a certain change once we upgraded to zope 3.4.
I also tried to get rid of webservice.txt's BaseAbsoluteURL class, but it turned out to be way too much work. I'll just use ILocation in the new service and change webservice.txt later.