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

Proposed by Leonard Richardson
Status: Merged
Approved by: Graham Binns
Approved revision: 158
Merged at revision: 169
Proposed branch: lp:~leonardr/lazr.restful/web-link-wadl
Merge into: lp:lazr.restful
Diff against target: 233 lines (+84/-24)
5 files modified
src/lazr/restful/_resource.py (+12/-8)
src/lazr/restful/docs/webservice.txt (+10/-6)
src/lazr/restful/tales.py (+4/-0)
src/lazr/restful/templates/wadl-root.pt (+7/-0)
src/lazr/restful/tests/test_webservice.py (+51/-10)
To merge this branch: bzr merge lp:~leonardr/lazr.restful/web-link-wadl
Reviewer Review Type Date Requested Status
Graham Binns (community) code Approve
Review via email: mp+47404@code.launchpad.net

Description of the change

My earlier lazr.restful branches added a web_link field to certain representations of entries. This branch finishes off the feature by adding the web_link field to the WADL description of every entry that supports it.

The changes to webservice.txt are necessary, even though they don't change the test, because WADL generation will no longer succeed unless every entry class has a publish_web_link annotation.

The test_wadl_includes_web_link_when_available test is a little awkward, but it was the best I could do within a unit test without writing a whole lot of code. Using wadllib didn't work, because it requires a WADL file that's fully navigable starting from the service root. The stub web services provided by WebServiceTestCase don't allow that.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote :

Hi Leonard,

Thanks for this branch. I found one small problem with it, which we discussed on IRC:

[15:42] gmb:
leonardr: In your diff it looks like there's a missing or extra single quote:
[15:43] gmb:
113+ <param style="plain" name="web_link" path="$[web_link']"
[15:43] gmb:
(specifically $[web_link'])
[15:43] leonardr:
gmb, thanks

Also, some of the formatting in the doctest was a bit confusing to me, so I've produced a patch that should take care of it (you don't have to apply this to land it, it's just a suggestion): http://pastebin.ubuntu.com/558144/plain/.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'src/lazr/restful/_resource.py'
--- src/lazr/restful/_resource.py 2011-01-24 17:28:14 +0000
+++ src/lazr/restful/_resource.py 2011-01-25 15:32:11 +0000
@@ -1415,12 +1415,10 @@
1415 """1415 """
1416 data = {}1416 data = {}
1417 data['self_link'] = absoluteURL(self.context, self.request)1417 data['self_link'] = absoluteURL(self.context, self.request)
1418 browser_request = queryAdapter(1418 if self.adapter_utility.publish_web_link:
1419 self.request, IWebBrowserOriginatingRequest)1419 # Objects in the web service correspond to pages on some website.
1420 if (browser_request is not None1420 # Provide the link to the corresponding page on the website.
1421 and self.adapter_utility.publish_web_link):1421 browser_request = IWebBrowserOriginatingRequest(self.request)
1422 # Objects in the web server correspond to objects on some website.
1423 # Provide the link to the correspnding object on the website.
1424 data['web_link'] = absoluteURL(self.context, browser_request)1422 data['web_link'] = absoluteURL(self.context, browser_request)
1425 data['resource_type_link'] = self.type_url1423 data['resource_type_link'] = self.type_url
1426 unmarshalled_field_values = {}1424 unmarshalled_field_values = {}
@@ -2156,8 +2154,14 @@
21562154
2157 @property2155 @property
2158 def publish_web_link(self):2156 def publish_web_link(self):
2159 """Should this object type have a web_link published?"""2157 """Return true if this entry should have a web_link."""
2160 return self._get_tagged_value('publish_web_link')2158 # If we can't adapt a web service request to a website
2159 # request, we shouldn't publish a web_link for *any* entry.
2160 web_service_request = get_current_web_service_request()
2161 website_request = queryAdapter(
2162 web_service_request, IWebBrowserOriginatingRequest)
2163 return website_request is not None and self._get_tagged_value(
2164 'publish_web_link')
21612165
2162 @property2166 @property
2163 def singular_type(self):2167 def singular_type(self):
21642168
=== modified file 'src/lazr/restful/docs/webservice.txt'
--- src/lazr/restful/docs/webservice.txt 2011-01-21 21:12:17 +0000
+++ src/lazr/restful/docs/webservice.txt 2011-01-25 15:32:11 +0000
@@ -555,13 +555,14 @@
555 >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME555 >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME
556 >>> class IAuthorEntry(IAuthor, IEntry):556 >>> class IAuthorEntry(IAuthor, IEntry):
557 ... """The part of an author we expose through the web service."""557 ... """The part of an author we expose through the web service."""
558 ... taggedValue(LAZR_WEBSERVICE_NAME, dict(singular="author",558 ... taggedValue(LAZR_WEBSERVICE_NAME, dict(
559 ... plural="authors"))559 ... singular="author", plural="authors", publish_web_link=True))
560560
561 >>> class ICommentEntry(IComment, IEntry):561 >>> class ICommentEntry(IComment, IEntry):
562 ... """The part of a comment we expose through the web service."""562 ... """The part of a comment we expose through the web service."""
563 ... taggedValue(LAZR_WEBSERVICE_NAME,563 ... taggedValue(LAZR_WEBSERVICE_NAME,
564 ... dict(singular="comment", plural="comments"))564 ... dict(singular="comment", plural="comments",
565 ... publish_web_link=True))
565566
566Most of the time, it doesn't work to expose to the web service the same data567Most of the time, it doesn't work to expose to the web service the same data
567model we expose internally. Usually there are fields we don't want to expose,568model we expose internally. Usually there are fields we don't want to expose,
@@ -582,7 +583,8 @@
582 ... "The part of a dish that we expose through the web service."583 ... "The part of a dish that we expose through the web service."
583 ... recipes = CollectionField(value_type=Reference(schema=IRecipe))584 ... recipes = CollectionField(value_type=Reference(schema=IRecipe))
584 ... taggedValue(LAZR_WEBSERVICE_NAME,585 ... taggedValue(LAZR_WEBSERVICE_NAME,
585 ... dict(singular="dish", plural="dishes"))586 ... dict(singular="dish", plural="dishes",
587 ... publish_web_link=True))
586588
587In the following code block we define an interface that exposes the underlying589In the following code block we define an interface that exposes the underlying
588``Recipe``'s name but not its ID. References to associated objects (like the590``Recipe``'s name but not its ID. References to associated objects (like the
@@ -596,7 +598,8 @@
596 ... instructions = Text(title=u"Name", required=True)598 ... instructions = Text(title=u"Name", required=True)
597 ... comments = CollectionField(value_type=Reference(schema=IComment))599 ... comments = CollectionField(value_type=Reference(schema=IComment))
598 ... taggedValue(LAZR_WEBSERVICE_NAME,600 ... taggedValue(LAZR_WEBSERVICE_NAME,
599 ... dict(singular="recipe", plural="recipes"))601 ... dict(singular="recipe", plural="recipes",
602 ... publish_web_link=True))
600603
601 >>> from lazr.restful.fields import ReferenceChoice604 >>> from lazr.restful.fields import ReferenceChoice
602 >>> class ICookbookEntry(IEntry):605 >>> class ICookbookEntry(IEntry):
@@ -608,7 +611,8 @@
608 ... comments = CollectionField(value_type=Reference(schema=IComment))611 ... comments = CollectionField(value_type=Reference(schema=IComment))
609 ... cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.")612 ... cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.")
610 ... taggedValue(LAZR_WEBSERVICE_NAME,613 ... taggedValue(LAZR_WEBSERVICE_NAME,
611 ... dict(singular="cookbook", plural="cookbooks"))614 ... dict(singular="cookbook", plural="cookbooks",
615 ... publish_web_link=True))
612616
613The ``author`` field is a choice between ``Author`` objects. To make sure617The ``author`` field is a choice between ``Author`` objects. To make sure
614that the ``Author`` objects are properly marshalled to JSON, we need to618that the ``Author`` objects are properly marshalled to JSON, we need to
615619
=== modified file 'src/lazr/restful/tales.py'
--- src/lazr/restful/tales.py 2011-01-21 21:12:17 +0000
+++ src/lazr/restful/tales.py 2011-01-25 15:32:11 +0000
@@ -441,6 +441,10 @@
441 return self.utility.entry_page_representation_id441 return self.utility.entry_page_representation_id
442442
443 @property443 @property
444 def publish_web_link(self):
445 return self.utility.publish_web_link
446
447 @property
444 def all_fields(self):448 def all_fields(self):
445 "Return all schema fields for the object."449 "Return all schema fields for the object."
446 return [field for name, field in450 return [field for name, field in
447451
=== modified file 'src/lazr/restful/templates/wadl-root.pt'
--- src/lazr/restful/templates/wadl-root.pt 2010-08-09 20:05:08 +0000
+++ src/lazr/restful/templates/wadl-root.pt 2011-01-25 15:32:11 +0000
@@ -223,6 +223,13 @@
223 <wadl:doc>The canonical link to this resource.</wadl:doc>223 <wadl:doc>The canonical link to this resource.</wadl:doc>
224 <link tal:attributes="resource_type context/wadl_entry:type_link" />224 <link tal:attributes="resource_type context/wadl_entry:type_link" />
225 </param>225 </param>
226 <param style="plain" name="web_link" path="$[web_link']"
227 tal:condition="context/wadl_entry:publish_web_link">
228 <wadl:doc>
229 The canonical human-addressable web link to this resource.
230 </wadl:doc>
231 <link />
232 </param>
226 <param style="plain" name="resource_type_link"233 <param style="plain" name="resource_type_link"
227 path="$['resource_type_link']">234 path="$['resource_type_link']">
228 <wadl:doc>235 <wadl:doc>
229236
=== modified file 'src/lazr/restful/tests/test_webservice.py'
--- src/lazr/restful/tests/test_webservice.py 2011-01-24 22:00:56 +0000
+++ src/lazr/restful/tests/test_webservice.py 2011-01-25 15:32:11 +0000
@@ -6,6 +6,7 @@
66
7from contextlib import contextmanager7from contextlib import contextmanager
8from cStringIO import StringIO8from cStringIO import StringIO
9from lxml import etree
9from operator import attrgetter10from operator import attrgetter
10from textwrap import dedent11from textwrap import dedent
11import random12import random
@@ -24,6 +25,8 @@
24 )25 )
25from zope.traversing.browser.interfaces import IAbsoluteURL26from zope.traversing.browser.interfaces import IAbsoluteURL
2627
28from wadllib.application import Application
29
27from lazr.enum import EnumeratedType, Item30from lazr.enum import EnumeratedType, Item
28from lazr.restful import ResourceOperation31from lazr.restful import ResourceOperation
29from lazr.restful.fields import Reference32from lazr.restful.fields import Reference
@@ -126,6 +129,8 @@
126class EntryTestCase(WebServiceTestCase):129class EntryTestCase(WebServiceTestCase):
127 """A test suite that defines an entry class."""130 """A test suite that defines an entry class."""
128131
132 WADL_NS = "{http://research.sun.com/wadl/2006/10}"
133
129 class DummyWebsiteRequest:134 class DummyWebsiteRequest:
130 """A request to the website, as opposed to the web service."""135 """A request to the website, as opposed to the web service."""
131 implements(IWebBrowserOriginatingRequest)136 implements(IWebBrowserOriginatingRequest)
@@ -135,20 +140,29 @@
135 URL = 'http://www.website.url/'140 URL = 'http://www.website.url/'
136141
137 @contextmanager142 @contextmanager
143 def request(self):
144 request = getUtility(IWebServiceConfiguration).createRequest(
145 StringIO(""), {})
146 newInteraction(request)
147 yield request
148 endInteraction()
149
150 @property
151 def wadl(self):
152 """Get a parsed WADL description of the web service."""
153 with self.request() as request:
154 return request.publication.application.toWADL().encode('utf-8')
155
156 @contextmanager
138 def entry_resource(self, entry_interface, entry_implementation):157 def entry_resource(self, entry_interface, entry_implementation):
139 """Create a request to an entry resource, and yield the resource."""158 """Create a request to an entry resource, and yield the resource."""
140 entry_class = get_resource_factory(entry_interface, IEntry)159 entry_class = get_resource_factory(entry_interface, IEntry)
141 request = getUtility(IWebServiceConfiguration).createRequest(
142 StringIO(""), {})
143 newInteraction(request)
144
145 data_object = entry_implementation("")160 data_object = entry_implementation("")
146 entry = entry_class(data_object, request)161
147 resource = EntryResource(data_object, request)162 with self.request() as request:
148163 entry = entry_class(data_object, request)
149 yield resource164 resource = EntryResource(data_object, request)
150165 yield resource
151 endInteraction()
152166
153 def _register_url_adapter(self, entry_interface):167 def _register_url_adapter(self, entry_interface):
154 """Register an IAbsoluteURL implementation for an interface."""168 """Register an IAbsoluteURL implementation for an interface."""
@@ -214,6 +228,26 @@
214 self.assertEquals(228 self.assertEquals(
215 representation['web_link'], self.DummyWebsiteURL.URL)229 representation['web_link'], self.DummyWebsiteURL.URL)
216230
231 def test_wadl_includes_web_link_when_available(self):
232 # If an entry includes a web_link, this information will
233 # show up in the WADL description of the entry.
234 service_root = "https://webservice_test/2.0/"
235 self._register_website_url_space(IHasOneField)
236
237 doc = etree.parse(StringIO(self.wadl))
238 # Verify that the 'has_one_field-full' representation includes
239 # a 'web_link' param.
240 representation = [
241 rep for rep in doc.findall('%srepresentation' % self.WADL_NS)
242 if rep.get('id') == 'has_one_field-full'][0]
243 param = [
244 param for param in representation.findall(
245 '%sparam' % self.WADL_NS)
246 if param.get('name') == 'web_link'][0]
247
248 # Verify that the 'web_link' param includes a 'link' tag.
249 self.assertFalse(param.find('%slink' % self.WADL_NS) is None)
250
217 def test_entry_omits_web_link_when_not_available(self):251 def test_entry_omits_web_link_when_not_available(self):
218 # When there is no way of turning a webservice request into a252 # When there is no way of turning a webservice request into a
219 # website request, the 'web_link' attribute is missing from253 # website request, the 'web_link' attribute is missing from
@@ -226,6 +260,13 @@
226 representation['self_link'], DummyAbsoluteURL.URL)260 representation['self_link'], DummyAbsoluteURL.URL)
227 self.assertFalse('web_link' in representation)261 self.assertFalse('web_link' in representation)
228262
263 def test_wadl_omits_web_link_when_not_available(self):
264 # When there is no way of turning a webservice request into a
265 # website request, the 'web_link' attribute is missing from
266 # WADL descriptions of entries.
267 self._register_url_adapter(IHasOneField)
268 self.assertFalse('web_link' in self.wadl)
269
229270
230class IHasNoWebLink(Interface):271class IHasNoWebLink(Interface):
231 """An entry that does not publish a web_link."""272 """An entry that does not publish a web_link."""

Subscribers

People subscribed via source and target branches