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
1=== modified file 'src/lazr/restful/_resource.py'
2--- src/lazr/restful/_resource.py 2011-01-24 17:28:14 +0000
3+++ src/lazr/restful/_resource.py 2011-01-25 15:32:11 +0000
4@@ -1415,12 +1415,10 @@
5 """
6 data = {}
7 data['self_link'] = absoluteURL(self.context, self.request)
8- browser_request = queryAdapter(
9- self.request, IWebBrowserOriginatingRequest)
10- if (browser_request is not None
11- and self.adapter_utility.publish_web_link):
12- # Objects in the web server correspond to objects on some website.
13- # Provide the link to the correspnding object on the website.
14+ if self.adapter_utility.publish_web_link:
15+ # Objects in the web service correspond to pages on some website.
16+ # Provide the link to the corresponding page on the website.
17+ browser_request = IWebBrowserOriginatingRequest(self.request)
18 data['web_link'] = absoluteURL(self.context, browser_request)
19 data['resource_type_link'] = self.type_url
20 unmarshalled_field_values = {}
21@@ -2156,8 +2154,14 @@
22
23 @property
24 def publish_web_link(self):
25- """Should this object type have a web_link published?"""
26- return self._get_tagged_value('publish_web_link')
27+ """Return true if this entry should have a web_link."""
28+ # If we can't adapt a web service request to a website
29+ # request, we shouldn't publish a web_link for *any* entry.
30+ web_service_request = get_current_web_service_request()
31+ website_request = queryAdapter(
32+ web_service_request, IWebBrowserOriginatingRequest)
33+ return website_request is not None and self._get_tagged_value(
34+ 'publish_web_link')
35
36 @property
37 def singular_type(self):
38
39=== modified file 'src/lazr/restful/docs/webservice.txt'
40--- src/lazr/restful/docs/webservice.txt 2011-01-21 21:12:17 +0000
41+++ src/lazr/restful/docs/webservice.txt 2011-01-25 15:32:11 +0000
42@@ -555,13 +555,14 @@
43 >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME
44 >>> class IAuthorEntry(IAuthor, IEntry):
45 ... """The part of an author we expose through the web service."""
46- ... taggedValue(LAZR_WEBSERVICE_NAME, dict(singular="author",
47- ... plural="authors"))
48+ ... taggedValue(LAZR_WEBSERVICE_NAME, dict(
49+ ... singular="author", plural="authors", publish_web_link=True))
50
51 >>> class ICommentEntry(IComment, IEntry):
52 ... """The part of a comment we expose through the web service."""
53 ... taggedValue(LAZR_WEBSERVICE_NAME,
54- ... dict(singular="comment", plural="comments"))
55+ ... dict(singular="comment", plural="comments",
56+ ... publish_web_link=True))
57
58 Most of the time, it doesn't work to expose to the web service the same data
59 model we expose internally. Usually there are fields we don't want to expose,
60@@ -582,7 +583,8 @@
61 ... "The part of a dish that we expose through the web service."
62 ... recipes = CollectionField(value_type=Reference(schema=IRecipe))
63 ... taggedValue(LAZR_WEBSERVICE_NAME,
64- ... dict(singular="dish", plural="dishes"))
65+ ... dict(singular="dish", plural="dishes",
66+ ... publish_web_link=True))
67
68 In the following code block we define an interface that exposes the underlying
69 ``Recipe``'s name but not its ID. References to associated objects (like the
70@@ -596,7 +598,8 @@
71 ... instructions = Text(title=u"Name", required=True)
72 ... comments = CollectionField(value_type=Reference(schema=IComment))
73 ... taggedValue(LAZR_WEBSERVICE_NAME,
74- ... dict(singular="recipe", plural="recipes"))
75+ ... dict(singular="recipe", plural="recipes",
76+ ... publish_web_link=True))
77
78 >>> from lazr.restful.fields import ReferenceChoice
79 >>> class ICookbookEntry(IEntry):
80@@ -608,7 +611,8 @@
81 ... comments = CollectionField(value_type=Reference(schema=IComment))
82 ... cover = Bytes(0, 5000, title=u"An image of the cookbook's cover.")
83 ... taggedValue(LAZR_WEBSERVICE_NAME,
84- ... dict(singular="cookbook", plural="cookbooks"))
85+ ... dict(singular="cookbook", plural="cookbooks",
86+ ... publish_web_link=True))
87
88 The ``author`` field is a choice between ``Author`` objects. To make sure
89 that the ``Author`` objects are properly marshalled to JSON, we need to
90
91=== modified file 'src/lazr/restful/tales.py'
92--- src/lazr/restful/tales.py 2011-01-21 21:12:17 +0000
93+++ src/lazr/restful/tales.py 2011-01-25 15:32:11 +0000
94@@ -441,6 +441,10 @@
95 return self.utility.entry_page_representation_id
96
97 @property
98+ def publish_web_link(self):
99+ return self.utility.publish_web_link
100+
101+ @property
102 def all_fields(self):
103 "Return all schema fields for the object."
104 return [field for name, field in
105
106=== modified file 'src/lazr/restful/templates/wadl-root.pt'
107--- src/lazr/restful/templates/wadl-root.pt 2010-08-09 20:05:08 +0000
108+++ src/lazr/restful/templates/wadl-root.pt 2011-01-25 15:32:11 +0000
109@@ -223,6 +223,13 @@
110 <wadl:doc>The canonical link to this resource.</wadl:doc>
111 <link tal:attributes="resource_type context/wadl_entry:type_link" />
112 </param>
113+ <param style="plain" name="web_link" path="$[web_link']"
114+ tal:condition="context/wadl_entry:publish_web_link">
115+ <wadl:doc>
116+ The canonical human-addressable web link to this resource.
117+ </wadl:doc>
118+ <link />
119+ </param>
120 <param style="plain" name="resource_type_link"
121 path="$['resource_type_link']">
122 <wadl:doc>
123
124=== modified file 'src/lazr/restful/tests/test_webservice.py'
125--- src/lazr/restful/tests/test_webservice.py 2011-01-24 22:00:56 +0000
126+++ src/lazr/restful/tests/test_webservice.py 2011-01-25 15:32:11 +0000
127@@ -6,6 +6,7 @@
128
129 from contextlib import contextmanager
130 from cStringIO import StringIO
131+from lxml import etree
132 from operator import attrgetter
133 from textwrap import dedent
134 import random
135@@ -24,6 +25,8 @@
136 )
137 from zope.traversing.browser.interfaces import IAbsoluteURL
138
139+from wadllib.application import Application
140+
141 from lazr.enum import EnumeratedType, Item
142 from lazr.restful import ResourceOperation
143 from lazr.restful.fields import Reference
144@@ -126,6 +129,8 @@
145 class EntryTestCase(WebServiceTestCase):
146 """A test suite that defines an entry class."""
147
148+ WADL_NS = "{http://research.sun.com/wadl/2006/10}"
149+
150 class DummyWebsiteRequest:
151 """A request to the website, as opposed to the web service."""
152 implements(IWebBrowserOriginatingRequest)
153@@ -135,20 +140,29 @@
154 URL = 'http://www.website.url/'
155
156 @contextmanager
157+ def request(self):
158+ request = getUtility(IWebServiceConfiguration).createRequest(
159+ StringIO(""), {})
160+ newInteraction(request)
161+ yield request
162+ endInteraction()
163+
164+ @property
165+ def wadl(self):
166+ """Get a parsed WADL description of the web service."""
167+ with self.request() as request:
168+ return request.publication.application.toWADL().encode('utf-8')
169+
170+ @contextmanager
171 def entry_resource(self, entry_interface, entry_implementation):
172 """Create a request to an entry resource, and yield the resource."""
173 entry_class = get_resource_factory(entry_interface, IEntry)
174- request = getUtility(IWebServiceConfiguration).createRequest(
175- StringIO(""), {})
176- newInteraction(request)
177-
178 data_object = entry_implementation("")
179- entry = entry_class(data_object, request)
180- resource = EntryResource(data_object, request)
181-
182- yield resource
183-
184- endInteraction()
185+
186+ with self.request() as request:
187+ entry = entry_class(data_object, request)
188+ resource = EntryResource(data_object, request)
189+ yield resource
190
191 def _register_url_adapter(self, entry_interface):
192 """Register an IAbsoluteURL implementation for an interface."""
193@@ -214,6 +228,26 @@
194 self.assertEquals(
195 representation['web_link'], self.DummyWebsiteURL.URL)
196
197+ def test_wadl_includes_web_link_when_available(self):
198+ # If an entry includes a web_link, this information will
199+ # show up in the WADL description of the entry.
200+ service_root = "https://webservice_test/2.0/"
201+ self._register_website_url_space(IHasOneField)
202+
203+ doc = etree.parse(StringIO(self.wadl))
204+ # Verify that the 'has_one_field-full' representation includes
205+ # a 'web_link' param.
206+ representation = [
207+ rep for rep in doc.findall('%srepresentation' % self.WADL_NS)
208+ if rep.get('id') == 'has_one_field-full'][0]
209+ param = [
210+ param for param in representation.findall(
211+ '%sparam' % self.WADL_NS)
212+ if param.get('name') == 'web_link'][0]
213+
214+ # Verify that the 'web_link' param includes a 'link' tag.
215+ self.assertFalse(param.find('%slink' % self.WADL_NS) is None)
216+
217 def test_entry_omits_web_link_when_not_available(self):
218 # When there is no way of turning a webservice request into a
219 # website request, the 'web_link' attribute is missing from
220@@ -226,6 +260,13 @@
221 representation['self_link'], DummyAbsoluteURL.URL)
222 self.assertFalse('web_link' in representation)
223
224+ def test_wadl_omits_web_link_when_not_available(self):
225+ # When there is no way of turning a webservice request into a
226+ # website request, the 'web_link' attribute is missing from
227+ # WADL descriptions of entries.
228+ self._register_url_adapter(IHasOneField)
229+ self.assertFalse('web_link' in self.wadl)
230+
231
232 class IHasNoWebLink(Interface):
233 """An entry that does not publish a web_link."""

Subscribers

People subscribed via source and target branches