Merge lp:~leonardr/lazr.restful/size-link into lp:lazr.restful

Proposed by Leonard Richardson
Status: Merged
Merged at revision: 137
Proposed branch: lp:~leonardr/lazr.restful/size-link
Merge into: lp:lazr.restful
Diff against target: 600 lines (+200/-56)
8 files modified
src/lazr/restful/_operation.py (+25/-2)
src/lazr/restful/_resource.py (+33/-6)
src/lazr/restful/declarations.py (+23/-12)
src/lazr/restful/docs/webservice-declarations.txt (+78/-21)
src/lazr/restful/example/base/interfaces.py (+1/-1)
src/lazr/restful/example/base/tests/collection.txt (+18/-7)
src/lazr/restful/example/base/tests/wadl.txt (+6/-5)
src/lazr/restful/templates/wadl-root.pt (+16/-2)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/size-link
Reviewer Review Type Date Requested Status
Benji York Pending
Review via email: mp+31060@code.launchpad.net

Description of the change

NOTE: this is not yet a real merge proposal because the branch is not yet ready. I'm handing the branch over to Benji, and a merge proposal is a convenient way to publicly describe the branch.

This branch makes it possible to define a named operation that returns a collection of objects, but doesn't provide a total_size. This is for cases where the total_size is expensive to calculate. In such cases, the JSON representation returned by the named operation includes a 'total_size_link' instead of a 'total_size'. A client that really wants to find out the total size can GET the total_size_link, which will invoke the operation again and _only_ return the total_size.

There are three things standing in the way of this branch being ready to land.

1. There's a test failure in django.txt.

2. There's a test for the new feature in examples/base, but there also needs to be one in webservice.txt. The test in examples/base tests operation adapters that were generated from declarations. We need a similar test in webservice.txt to test operation adapters that were defined by hand--those adapters go through a different code path.

(I'm confident that this new test will pass immediately, because I ironed out the bugs in this code path when another bug caused *all* named operations to serve total_size_link instead of total_size.)

3. Most importantly, it's not clear as a matter of policy when we should serve total_size_link instead of total_size. I implemented this branch to serve total_size_link when a named operation is annotated with @operation_returns_collection_of(..., include_total_size=False). But there's an active debate on launchpad-dev with implications for this branch. In particular, there's a good argument to be made that we should be serving total_size_link instead of total_size for *all* named operations, with the following exceptions:

 a. We should maintain the old behavior for the 'beta' and '1.0' web services.
 b. If we know that the entire list fits in one batch, we might as well serve total_size.

This question needs to be decided, and if it's decided in favor of always serving total_size_link (as IMO it should), then the annotation code needs to be removed and replaced with code that gives the old behavior up to a configurable web service version, and the new behavior afterwards.

To post a comment you must log in.
lp:~leonardr/lazr.restful/size-link updated
139. By Leonard Richardson

Mention total_size_link in the WADL.

Revision history for this message
Leonard Richardson (leonardr) wrote :

While working on the lazr.restfulclient branch I realized another problem with this branch: the WADL doesn't mention total_size_link, so lazr.restfulclient can't pick it up. I changed the description of each 'page' representation to mention both 'total_size' and 'total_size_link', since there's no way of knowing which will be present for any given collection. (Not without multiplying the number of 'page' representations by two, which is stupid, or serving them dynamically, which we could do but it's not worth it.)

lp:~leonardr/lazr.restful/size-link updated
140. By Leonard Richardson

Renamed the 'number' type to the 'scalar value' type.

141. By Leonard Richardson

Link to #ScalarValue instead of now-nonexistent #Number.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/lazr/restful/_operation.py'
2--- src/lazr/restful/_operation.py 2010-04-20 18:26:49 +0000
3+++ src/lazr/restful/_operation.py 2010-07-28 13:24:44 +0000
4@@ -12,6 +12,7 @@
5 from zope.schema.interfaces import (
6 IField, RequiredMissing, ValidationError, WrongType)
7 from zope.security.proxy import isinstance as zope_isinstance
8+from zope.traversing.browser import absoluteURL
9
10 from lazr.lifecycle.event import ObjectModifiedEvent
11 from lazr.lifecycle.snapshot import Snapshot
12@@ -44,6 +45,23 @@
13 def __init__(self, context, request):
14 self.context = context
15 self.request = request
16+ self.total_size_only = False
17+
18+ def total_size_link(self, navigator):
19+ """Return a link to the total size of a collection."""
20+ if getattr(self, 'include_total_size', True):
21+ # This is a named operation that includes the total size
22+ # inline rather than with a link.
23+ return None
24+ if not IResourceGETOperation.providedBy(self):
25+ # Only GET operations can have their total size split out into
26+ # a link, because only GET operations are safe.
27+ return None
28+ base = str(self.request.URL)
29+ query = navigator.getCleanQueryString()
30+ if query != '':
31+ query += '&'
32+ return base + '?' + query + "ws.show=total_size"
33
34 def __call__(self):
35 values, errors = self.validate()
36@@ -84,9 +102,14 @@
37 # If the result is a web service collection, serve only one
38 # batch of the collection.
39 collection = getMultiAdapter((result, self.request), ICollection)
40- result = CollectionResource(collection, self.request).batch() + '}'
41+ resource = CollectionResource(collection, self.request)
42+ result = resource.batch(total_size_only=self.total_size_only)
43+ if not self.total_size_only:
44+ result += '}'
45 elif self.should_batch(result):
46- result = self.batch(result, self.request) + '}'
47+ result = self.batch(result, self.request, self.total_size_only)
48+ if not self.total_size_only:
49+ result += '}'
50 else:
51 # Serialize the result to JSON. Any embedded entries will be
52 # automatically serialized.
53
54=== modified file 'src/lazr/restful/_resource.py'
55--- src/lazr/restful/_resource.py 2010-06-14 15:24:20 +0000
56+++ src/lazr/restful/_resource.py 2010-07-28 13:24:44 +0000
57@@ -597,19 +597,34 @@
58 # super() even though this class itself has no superclass.
59 super(BatchingResourceMixin, self).__init__(context, request)
60
61- def batch(self, entries, request):
62+ def total_size_link(self, navigator):
63+ """Return the URL to fetch to find out the collection's total size.
64+
65+ If this is None, the total size will be included inline.
66+
67+ :param navigator: A BatchNavigator object for the current batch.
68+ """
69+ return None
70+
71+ def batch(self, entries, request, total_size_only=False):
72 """Prepare a batch from a (possibly huge) list of entries.
73
74- :return: A JSON string representing a hash:
75+ :return: If size_only is True, a JSON string
76+ representing the number of objects in the list as a whole. If
77+ size_only is False, a JSON string representing a hash:
78+
79 'entries' contains a list of EntryResource objects for the
80 entries that actually made it into this batch
81 'total_size' contains the total size of the list.
82+ 'total_size_link' contains a link to the total size of the list.
83 'next_url', if present, contains a URL to get the next batch
84 in the list.
85 'prev_url', if present, contains a URL to get the previous batch
86 in the list.
87 'start' contains the starting index of this batch
88
89+ Only one of 'total_size' or 'total_size_link' will be present.
90+
91 Note that the JSON string will be missing its final curly
92 brace. This is in case the caller wants to add some additional
93 keys to the JSON hash. It's the caller's responsibility to add
94@@ -619,12 +634,19 @@
95 entries = IFiniteSequence(entries)
96 navigator = WebServiceBatchNavigator(entries, request)
97
98+ if total_size_only:
99+ return simplejson.dumps(navigator.batch.listlength)
100+
101 view_permission = getUtility(IWebServiceConfiguration).view_permission
102 resources = [EntryResource(entry, request)
103 for entry in navigator.batch
104 if checkPermission(view_permission, entry)]
105- batch = { 'total_size' : navigator.batch.listlength,
106- 'start' : navigator.batch.start }
107+ batch = { 'start' : navigator.batch.start }
108+ total_size_link = self.total_size_link(navigator)
109+ if total_size_link is None:
110+ batch['total_size'] = navigator.batch.listlength
111+ else:
112+ batch['total_size_link'] = total_size_link
113 if navigator.batch.start < 0:
114 batch['start'] = None
115 next_url = navigator.nextBatchURL()
116@@ -674,6 +696,10 @@
117 except ComponentLookupError:
118 self.request.response.setStatus(400)
119 return "No such operation: " + operation_name
120+
121+ show = self.request.form.get('ws.show')
122+ if show == 'total_size':
123+ operation.total_size_only = True
124 return operation()
125
126 def handleCustomPOST(self, operation_name):
127@@ -1663,14 +1689,15 @@
128 self.request.response.setHeader('Content-type', self.JSON_TYPE)
129 return result
130
131- def batch(self, entries=None):
132+ def batch(self, entries=None, total_size_only=False):
133 """Return a JSON representation of a batch of entries.
134
135 :param entries: (Optional) A precomputed list of entries to batch.
136 """
137 if entries is None:
138 entries = self.collection.find()
139- result = super(CollectionResource, self).batch(entries, self.request)
140+ result = super(CollectionResource, self).batch(
141+ entries, self.request, total_size_only)
142 result += (
143 ', "resource_type_link" : ' + simplejson.dumps(self.type_url)
144 + '}')
145
146=== modified file 'src/lazr/restful/declarations.py'
147--- src/lazr/restful/declarations.py 2010-05-07 14:02:44 +0000
148+++ src/lazr/restful/declarations.py 2010-07-28 13:24:44 +0000
149@@ -707,20 +707,24 @@
150 class operation_returns_collection_of(_method_annotator):
151 """Specify that the exported operation returns a collection.
152
153- The decorator takes a single argument: an interface that's been
154- exported as an entry.
155+ The decorator takes one required argument, "schema", an interface
156+ that's been exported as an entry. It takes one optional argument,
157+ "include_total_size", which controls whether the total size of the
158+ resultset is included inline, or whether the client has to follow
159+ a link to find it (because it's very expensive to calculate).
160 """
161-
162- def __init__(self, schema):
163+ def __init__(self, schema, include_total_size=True):
164 _check_called_from_interface_def('%s()' % self.__class__.__name__)
165 if not IInterface.providedBy(schema):
166 raise TypeError('Collection value type %s does not provide '
167 'IInterface.' % schema)
168 self.return_type = CollectionField(
169 value_type=Reference(schema=schema))
170+ self.include_total_size = include_total_size
171
172 def annotate_method(self, method, annotations):
173 annotations['return_type'] = self.return_type
174+ annotations['include_total_size'] = self.include_total_size
175
176
177 class _export_operation(_method_annotator):
178@@ -1173,23 +1177,22 @@
179 version = getUtility(IWebServiceConfiguration).active_versions[0]
180
181 bases = (BaseResourceOperationAdapter, )
182- if tag['type'] == 'read_operation':
183+ operation_type = tag['type']
184+ if operation_type == 'read_operation':
185 prefix = 'GET'
186 provides = IResourceGETOperation
187- elif tag['type'] in ('factory', 'write_operation'):
188+ elif operation_type in ('factory', 'write_operation'):
189 provides = IResourcePOSTOperation
190 prefix = 'POST'
191- if tag['type'] == 'factory':
192+ if operation_type == 'factory':
193 bases = (BaseFactoryResourceOperationAdapter,)
194- elif tag['type'] == 'destructor':
195+ elif operation_type == 'destructor':
196 provides = IResourceDELETEOperation
197 prefix = 'DELETE'
198 else:
199- raise AssertionError('Unknown method export type: %s' % tag['type'])
200+ raise AssertionError('Unknown method export type: %s' % operation_type)
201
202 return_type = tag['return_type']
203- if return_type is None:
204- return_type = None
205
206 name = _versioned_class_name(
207 '%s_%s_%s' % (prefix, method.interface.__name__, tag['as']),
208@@ -1200,7 +1203,15 @@
209 '_method_name': method.__name__,
210 '__doc__': method.__doc__}
211
212- if tag['type'] == 'write_operation':
213+ if isinstance(return_type, CollectionField):
214+ include_total_size = tag.get('include_total_size', True)
215+ if not include_total_size and operation_type != 'read_operation':
216+ raise AssertionError(
217+ "Only read operations can decline to set "
218+ "include_total_size.")
219+ class_dict['include_total_size'] = include_total_size
220+
221+ if operation_type == 'write_operation':
222 class_dict['send_modification_event'] = True
223 factory = type(name, bases, class_dict)
224 classImplements(factory, provides)
225
226=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
227--- src/lazr/restful/docs/webservice-declarations.txt 2010-05-07 15:32:27 +0000
228+++ src/lazr/restful/docs/webservice-declarations.txt 2010-07-28 13:24:44 +0000
229@@ -32,7 +32,7 @@
230 field, but not the inventory_number field.
231
232 >>> from zope.interface import Interface
233- >>> from zope.schema import TextLine, Float
234+ >>> from zope.schema import Text, TextLine, Float
235 >>> from lazr.restful.declarations import (
236 ... export_as_webservice_entry, exported)
237 >>> class IBook(Interface):
238@@ -48,6 +48,8 @@
239 ... exported_as='price')
240 ...
241 ... inventory_number = TextLine(title=u'The inventory part number.')
242+ ...
243+ ... text = exported(Text(title=u'The text of the book'))
244
245 These declarations add tagged values to the original interface elements.
246 The tags are in the lazr.restful namespace and are dictionaries of
247@@ -212,7 +214,7 @@
248 TypeError: export_as_webservice_collection() is missing a method
249 tagged with @collection_default_content.
250
251-As it is an error, to mark more than one methods:
252+As it is an error, to mark more than one method:
253
254 >>> class TwoDefaultContent(Interface):
255 ... export_as_webservice_collection(IDummyInterface)
256@@ -329,8 +331,15 @@
257 ... text=copy_field(IBook['title'], title=u'Text to search for.'))
258 ... @operation_returns_collection_of(IBook)
259 ... @export_read_operation()
260- ... def searchBooks(text):
261- ... """Return list of books containing 'text'."""
262+ ... def searchBookTitles(text):
263+ ... """Return list of books whose titles contain 'text'."""
264+ ...
265+ ... @operation_parameters(
266+ ... text=copy_field(IBook['text'], title=u'Text to search for.'))
267+ ... @operation_returns_collection_of(IBook, include_total_size=False)
268+ ... @export_read_operation()
269+ ... def searchBookText(text):
270+ ... """Return list of books whose text contains 'text'."""
271 ...
272 ... @operation_parameters(
273 ... text=copy_field(IBook['title'], title=u'Text to search for.'))
274@@ -392,16 +401,32 @@
275 return_type: <lazr.restful._operation.ObjectLink object...>
276 type: 'factory'
277
278-We did specify the return type for the 'searchBooks' method: it
279-returns a collection.
280+We did specify the return type for the 'searchBookTitles' method: it
281+returns a collection. Searching the titles of books is cheap, so when
282+lazr.restful serves a page of results, it can include the total size
283+of the collection.
284
285- >>> print_export_tag(IBookSetOnSteroids['searchBooks'])
286- as: 'searchBooks'
287+ >>> print_export_tag(IBookSetOnSteroids['searchBookTitles'])
288+ as: 'searchBookTitles'
289 call_with: {}
290+ include_total_size: True
291 params: {'text': <...TextLine...>}
292 return_type: <lazr.restful.fields.CollectionField object...>
293 type: 'read_operation'
294
295+The 'searchBookText' method returns the same kind of
296+collection. Searching the full text of books is quite expensive, so
297+when lazr.restful searches a page of results, we've told it not to
298+include the total size of the collection.
299+
300+ >>> print_export_tag(IBookSetOnSteroids['searchBookText'])
301+ as: 'searchBookText'
302+ call_with: {}
303+ include_total_size: False
304+ params: {'text': <...Text...>}
305+ return_type: <lazr.restful.fields.CollectionField object...>
306+ type: 'read_operation'
307+
308 The 'bestMatch' method returns an entry.
309
310 >>> print_export_tag(IBookSetOnSteroids['bestMatch'])
311@@ -817,6 +842,7 @@
312 >>> dump_entry_interface(entry_interface)
313 author: TextLine
314 price: Float
315+ text: Text
316 title: TextLine
317
318 The field __name__ attribute contains the exported name:
319@@ -888,9 +914,11 @@
320 >>> class Book(object):
321 ... """Simple IBook implementation."""
322 ... implements(IBook)
323- ... def __init__(self, author, title, base_price, inventory_number):
324+ ... def __init__(self, author, title, text, base_price,
325+ ... inventory_number):
326 ... self.author = author
327 ... self.title = title
328+ ... self.text = text
329 ... self.base_price = base_price
330 ... self.inventory_number = inventory_number
331
332@@ -942,7 +970,7 @@
333 IBookEntry.
334
335 >>> entry_adapter = entry_adapter_factory(
336- ... Book(u'Aldous Huxley', u'Island', 10.0, '12345'),
337+ ... Book(u'Aldous Huxley', u'Island', 'Text here', 10.0, '12345'),
338 ... request)
339
340 >>> entry_adapter.schema is entry_interface
341@@ -1058,7 +1086,7 @@
342 ... generate_operation_adapter)
343
344 >>> read_method_adapter_factory = generate_operation_adapter(
345- ... IBookSetOnSteroids['searchBooks'])
346+ ... IBookSetOnSteroids['searchBookTitles'])
347 >>> IResourceGETOperation.implementedBy(read_method_adapter_factory)
348 True
349
350@@ -1070,14 +1098,14 @@
351
352 >>> from lazr.restful import ResourceOperation
353 >>> read_method_adapter_factory.__name__
354- 'GET_IBookSetOnSteroids_searchBooks_beta'
355+ 'GET_IBookSetOnSteroids_searchBookTitles_beta'
356 >>> issubclass(read_method_adapter_factory, ResourceOperation)
357 True
358
359 The adapter's docstring is taken from the decorated method docstring.
360
361 >>> read_method_adapter_factory.__doc__
362- "Return list of books containing 'text'."
363+ "Return list of books whose titles contain 'text'."
364
365 The adapter's params attribute contains the specification of the
366 parameters accepted by the operation.
367@@ -1097,11 +1125,15 @@
368 ...
369 ... result = None
370 ...
371- ... def searchBooks(self, text):
372+ ... def searchBookTitles(self, text):
373+ ... return self.result
374+ ...
375+ ... def searchBookText(self, text):
376 ... return self.result
377 ...
378 ... def new(self, author, base_price, title):
379- ... return Book(author, title, base_price, "unknown")
380+ ... return Book(author, title, "default text", base_price,
381+ ... "unknown")
382
383 Now we can create a fake request that invokes the named operation.
384
385@@ -1148,7 +1180,8 @@
386
387 >>> write_method_adapter = write_method_adapter_factory(
388 ... BookOnSteroids(
389- ... 'Aldous Huxley', 'The Doors of Perception', 8, 'unknown'),
390+ ... 'Aldous Huxley', 'The Doors of Perception',
391+ ... 'The text', 8, 'unknown'),
392 ... FakeRequest())
393
394 >>> verifyObject(IResourcePOSTOperation, write_method_adapter)
395@@ -2447,8 +2480,8 @@
396 # ProxyFactory wraps the content using the defined checker.
397 >>> print debug_proxy(ProxyFactory(entry_adapter))
398 zope.security._proxy._Proxy (using zope.security.checker.Checker)
399- public: author, price, schema, title
400- public (set): author, price, schema, title
401+ public: author, price, schema, text, title
402+ public (set): author, price, schema, text, title
403
404 >>> print debug_proxy(ProxyFactory(collection_adapter))
405 zope.security._proxy._Proxy (using zope.security.checker.Checker)
406@@ -2511,7 +2544,8 @@
407 ICollection are available:
408
409 >>> from zope.component import getMultiAdapter
410- >>> book = Book(u'George Orwell', u'1984', 10.0, u'12345-1984')
411+ >>> book = Book(u'George Orwell', u'1984', 'Text here', 10.0,
412+ ... u'12345-1984')
413 >>> bookset = BookSet([book])
414
415 >>> entry_adapter = getMultiAdapter((book, request), IEntry)
416@@ -2537,8 +2571,12 @@
417 >>> request_interface = IWebServiceClientRequest
418 >>> adapter_registry.lookup(
419 ... (IBookSetOnSteroids, request_interface),
420- ... IResourceGETOperation, 'searchBooks')
421- <class '...GET_IBookSetOnSteroids_searchBooks_beta'>
422+ ... IResourceGETOperation, 'searchBookTitles')
423+ <class '...GET_IBookSetOnSteroids_searchBookTitles_beta'>
424+ >>> adapter_registry.lookup(
425+ ... (IBookSetOnSteroids, request_interface),
426+ ... IResourceGETOperation, 'searchBookText')
427+ <class '...GET_IBookSetOnSteroids_searchBookText_beta'>
428 >>> adapter_registry.lookup(
429 ... (IBookSetOnSteroids, request_interface),
430 ... IResourcePOSTOperation, 'create_book')
431@@ -2619,6 +2657,25 @@
432 that version. The bad annotations are: "params", "as".
433 ...
434
435+Here's a class that tries to define a write operation that returns a
436+collection without including the total size of the collection.
437+lazr.restful prohibits this: write operations are unsafe, and whatever
438+collection a write operation might return, retrieving its size in a
439+separate request would mean changing the dataset twice.
440+
441+ >>> class IWriteOperationWithoutTotalSize(Interface):
442+ ... export_as_webservice_entry()
443+ ... @operation_returns_collection_of(IBook, include_total_size=False)
444+ ... @export_write_operation()
445+ ... def a_method(**kwargs): pass
446+ >>> register_test_module('badwrite', IWriteOperationWithoutTotalSize)
447+ Traceback (most recent call last):
448+ ...
449+ ConfigurationExecutionError: ... Only read operations can decline
450+ to set include_total_size.
451+ ...
452+
453+
454 Mutators as named operations
455 ----------------------------
456
457
458=== modified file 'src/lazr/restful/example/base/interfaces.py'
459--- src/lazr/restful/example/base/interfaces.py 2010-02-15 14:53:11 +0000
460+++ src/lazr/restful/example/base/interfaces.py 2010-07-28 13:24:44 +0000
461@@ -188,7 +188,7 @@
462 search=TextLine(title=u"String to search for in recipe name."),
463 vegetarian=Bool(title=u"Whether or not to limit the search to "
464 "vegetarian cookbooks.", default=False))
465- @operation_returns_collection_of(IRecipe)
466+ @operation_returns_collection_of(IRecipe, include_total_size=False)
467 @export_read_operation()
468 def find_recipes(search, vegetarian):
469 """Search for recipes across cookbooks."""
470
471=== modified file 'src/lazr/restful/example/base/tests/collection.txt'
472--- src/lazr/restful/example/base/tests/collection.txt 2009-04-24 15:14:07 +0000
473+++ src/lazr/restful/example/base/tests/collection.txt 2010-07-28 13:24:44 +0000
474@@ -173,14 +173,12 @@
475 ... "/cookbooks?ws.op=find_recipes&%s" % args).jsonBody()
476
477 >>> s_recipes = search_recipes("chicken")
478- >>> s_recipes['total_size']
479- 3
480 >>> sorted(r['instructions'] for r in s_recipes['entries'])
481 [u'Draw, singe, stuff, and truss...', u'You can always judge...']
482
483 >>> veg_recipes = search_recipes("chicken", True)
484- >>> veg_recipes['total_size']
485- 0
486+ >>> veg_recipes['entries']
487+ []
488
489 A custom operation that returns a list of objects is paginated, just
490 like a collection.
491@@ -196,11 +194,22 @@
492 empty list of results:
493
494 >>> empty_collection = search_recipes("nosuchrecipe")
495- >>> empty_collection['total_size']
496- 0
497 >>> [r['instructions'] for r in empty_collection['entries']]
498 []
499
500+Some named operations that return a collection include the total size
501+of the collection inline. The 'search_recipes' operation includes a
502+_link_ to the total size of the collection.
503+
504+ >>> print s_recipes['total_size_link']
505+ http://.../cookbooks?search=chicken&vegetarian=false&ws.op=find_recipes&ws.show=total_size
506+
507+Sending a GET request to that link yields a JSON representation of the
508+total size.
509+
510+ >>> print webservice.get(s_recipes['total_size_link']).jsonBody()
511+ 3
512+
513 Custom operations may have error handling. In this case, the error
514 handling is in the validate() method of the 'search' field.
515
516@@ -216,9 +225,11 @@
517
518 >>> general_cookbooks = webservice.get(
519 ... "/cookbooks?ws.op=find_for_cuisine&cuisine=General")
520- >>> general_cookbooks.jsonBody()['total_size']
521+ >>> print general_cookbooks.jsonBody()['total_size']
522 3
523
524+
525+
526 POST operations
527 ===============
528
529
530=== modified file 'src/lazr/restful/example/base/tests/wadl.txt'
531--- src/lazr/restful/example/base/tests/wadl.txt 2010-03-10 18:45:04 +0000
532+++ src/lazr/restful/example/base/tests/wadl.txt 2010-07-28 13:24:44 +0000
533@@ -537,8 +537,9 @@
534 'entry_resource_descriptions'.
535
536 >>> entry_resource_descriptions = []
537- >>> entry_resource_types = other_children[first_entry_type_index:-2]
538- >>> hosted_binary_resource_type, simple_binary_type = other_children[-2:]
539+ >>> entry_resource_types = other_children[first_entry_type_index:-3]
540+ >>> (hosted_binary_resource_type, scalar_type, simple_binary_type
541+ ... ) = other_children[-3:]
542 >>> for index in range(0, len(entry_resource_types), 5):
543 ... entry_resource_descriptions.append(
544 ... (tuple(entry_resource_types[index:index + 5])))
545@@ -1155,9 +1156,9 @@
546 All collection representations have the same five <param> tags.
547
548 >>> [param.attrib['name'] for param in collection_rep]
549- ['resource_type_link', 'total_size', 'start', 'next_collection_link',
550- 'prev_collection_link', 'entries', 'entry_links']
551- >>> (type_link, size, start, next, prev, entries,
552+ ['resource_type_link', 'total_size', 'total_size_link', 'start',
553+ 'next_collection_link', 'prev_collection_link', 'entries', 'entry_links']
554+ >>> (type_link, size, size_link, start, next, prev, entries,
555 ... entry_links) = collection_rep
556
557 So what's the difference between a collection of people and a
558
559=== modified file 'src/lazr/restful/templates/wadl-root.pt'
560--- src/lazr/restful/templates/wadl-root.pt 2010-03-10 18:50:27 +0000
561+++ src/lazr/restful/templates/wadl-root.pt 2010-07-28 13:24:44 +0000
562@@ -292,7 +292,12 @@
563 </param>
564
565 <param style="plain" name="total_size" path="$['total_size']"
566- required="true" />
567+ required="false" />
568+
569+ <param style="plain" name="total_size_link" path="$['total_size_link']"
570+ required="false">
571+ <link resource_type="#ScalarValue" />
572+ </param>
573
574 <param style="plain" name="start" path="$['start']" required="true" />
575
576@@ -321,7 +326,7 @@
577 <!--End representation and resource_type definitions for entry
578 resources. -->
579
580- <!--Finally, describe the 'hosted binary file' type.-->
581+ <!--Finally, describe the 'hosted binary file' type...-->
582 <resource_type id="HostedFile">
583 <method name="GET" id="HostedFile-get">
584 <response>
585@@ -334,6 +339,15 @@
586 <method name="DELETE" id="HostedFile-delete" />
587 </resource_type>
588
589+ <!--...and the simple 'scalar value' type.-->
590+ <resource_type id="ScalarValue">
591+ <method name="GET" id="ScalarValue-get">
592+ <response>
593+ <representation mediaType="application/json" />
594+ </response>
595+ </method>
596+ </resource_type>
597+
598 <!--Define a data type for binary data.-->
599 <xsd:simpleType name="binary">
600 <xsd:list itemType="byte" />

Subscribers

People subscribed via source and target branches