Merge lp://staging/~leonardr/lazr.restful/multiversion-collection into lp://staging/lazr.restful
- multiversion-collection
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Francis J. Lacoste |
Approved revision: | no longer in the revision history of the source branch. |
Merged at revision: | not available |
Proposed branch: | lp://staging/~leonardr/lazr.restful/multiversion-collection |
Merge into: | lp://staging/lazr.restful |
Prerequisite: | lp://staging/~leonardr/lazr.restful/version-specific-request-interface |
Diff against target: |
2348 lines (+1050/-316) 20 files modified
src/lazr/restful/NEWS.txt (+5/-4) src/lazr/restful/_operation.py (+5/-5) src/lazr/restful/_resource.py (+64/-27) src/lazr/restful/declarations.py (+11/-5) src/lazr/restful/directives/__init__.py (+23/-3) src/lazr/restful/docs/multiversion.txt (+625/-144) src/lazr/restful/docs/utils.txt (+27/-5) src/lazr/restful/docs/webservice-declarations.txt (+49/-26) src/lazr/restful/docs/webservice-error.txt (+1/-0) src/lazr/restful/docs/webservice.txt (+60/-36) src/lazr/restful/example/base/root.py (+5/-4) src/lazr/restful/interfaces/_rest.py (+16/-1) src/lazr/restful/metazcml.py (+7/-1) src/lazr/restful/publisher.py (+15/-7) src/lazr/restful/simple.py (+4/-1) src/lazr/restful/tales.py (+19/-9) src/lazr/restful/testing/webservice.py (+5/-1) src/lazr/restful/tests/test_navigation.py (+50/-29) src/lazr/restful/tests/test_webservice.py (+42/-8) src/lazr/restful/utils.py (+17/-0) |
To merge this branch: | bzr merge lp://staging/~leonardr/lazr.restful/multiversion-collection |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Francis J. Lacoste (community) | code | Approve | |
Review via email: mp+16924@code.staging.launchpad.net |
Commit message
Description of the change
Leonard Richardson (leonardr) wrote : | # |
Leonard Richardson (leonardr) wrote : | # |
I fixed all but one of the XXX sections (the remaining XXX can't be removed until we can generate multiple versions from declarations) and removed some useless code.
Francis J. Lacoste (flacoste) wrote : | # |
On January 7, 2010, Leonard Richardson wrote:
> I fixed all but one of the XXX sections (the remaining XXX can't be removed
> until we can generate multiple versions from declarations) and removed
> some useless code.
>
Hi Leonard,
This is really taking shape! I have a few questions and suggestions, so
setting this to
review needsfixing code
> === modified file 'src/lazr/
> class EntryFieldURL(
> """An IAbsoluteURL adapter for EntryField objects."""
> component.
> @@ -1243,7 +1246,7 @@
> def __init__(self, context, request):
> """Associate this resource with a specific object and request."""
> super(EntryReso
> - self.entry = IEntry(context)
> + self.entry = getMultiAdapter
>
Not that if context is already providing IEntry, you'll get an error. I don't
know if this is a plausible case, but if it is, you'll want to only query the
adapter, if context doesn't provide IEntry.
> # We are given a model schema (IFoo). Look up the
> # corresponding entry schema (IFooEntry).
> model_schema = self.relationsh
> - return getGlobalSiteMa
> - model_schema, IEntry).schema
> + request_interface = getUtility(
> + IVersionedClien
> + name=self.
> + return getGlobalSiteMa
> + (model_schema, request_interface), IEntry).schema
In my previous review, I suggested I...Factory instead of
IVersionedClien
Sine you are using the name, why do you need a separate interface. Why not
simply ask for the IWebClientRequest with the specific name? Since all
versioned interface should extend that one, it should work.
> === modified file 'src/lazr/
> + utility = cls()
> + sm = getSiteManager()
> + sm.registerUtil
> +
> + # Create and register marker interfaces for request objects.
> + for version in set(
> + utility.
> + classname = ("IWebServiceCl
> + version.
There are probably other characters that need to be squashed '-' is likely to
be used.
> + marker_interface = InterfaceClass(
> + classname, (IWebServiceCli
> + alsoProvides(
> + marker_interface, IVersionedClien
> + sm.registerUtility(
> + marker_interface, IVersionedClien
> + name=version)
> return True
I suggest writing a unit test for that part that would cover the mangling.
> === modified file 'src/lazr/
> +URL generation
> +--------------
> +
> + >>> from zope.component import getSiteManager
> + >>> from zope.traversing
Leonard Richardson (leonardr) wrote : | # |
Thanks for your thorough review. I'm going through it slowly. I'll probably have many comments in reply.
> > # We are given a model schema (IFoo). Look up the
> > # corresponding entry schema (IFooEntry).
> > model_schema = self.relationsh
> > - return getGlobalSiteMa
> > - model_schema, IEntry).schema
> > + request_interface = getUtility(
> > + IVersionedClien
> > + name=self.
> > + return getGlobalSiteMa
> > + (model_schema, request_interface), IEntry).schema
>
> In my previous review, I suggested I...Factory instead of
> IVersionedClien
> Sine you are using the name, why do you need a separate interface. Why not
> simply ask for the IWebClientRequest with the specific name? Since all
> versioned interface should extend that one, it should work.
I don't do that because request objects shouldn't be utilities. There's not one request object for every version, but one object for every request. I could create a dummy request object and register that as a utility, but I think that would be really confusing. What I have is also confusing, but it can be explained in an internally consistent way:
1. We need a way to look up a web service description given a version number.
2. These lookups don't operate by version number; they take a marker interface that's different for every version.
3. So we create a way to look up the marker interface given the version number.
4. We implement this by registering the marker interface objects as named utilities.
Does this make sense?
I believe in all these cases we have a specific request object whose .version we look up. If there was some way of extracting from the request object the marker interface that identified the version, we could write a property method WebServiceClien
Francis J. Lacoste (flacoste) wrote : | # |
On January 11, 2010, Leonard Richardson wrote:
> Thanks for your thorough review. I'm going through it slowly. I'll probably
> have many comments in reply.
>
> > > # We are given a model schema (IFoo). Look up the
> > > # corresponding entry schema (IFooEntry).
> > > model_schema = self.relationsh
> > > - return getGlobalSiteMa
> > > - model_schema, IEntry).schema
> > > + request_interface = getUtility(
> > > + IVersionedClien
> > > + name=self.
> > > + return getGlobalSiteMa
> > > + (model_schema, request_interface), IEntry).schema
> >
> > In my previous review, I suggested I...Factory instead of
> > IVersionedClien
> > factory. Sine you are using the name, why do you need a separate
> > interface. Why not simply ask for the IWebClientRequest with the specific
> > name? Since all versioned interface should extend that one, it should
> > work.
>
> I don't do that because request objects shouldn't be utilities. There's not
> one request object for every version, but one object for every request. I
> could create a dummy request object and register that as a utility, but I
> think that would be really confusing. What I have is also confusing, but
> it can be explained in an internally consistent way:
>
> 1. We need a way to look up a web service description given a version
> number. 2. These lookups don't operate by version number; they take a
> marker interface that's different for every version. 3. So we create a way
> to look up the marker interface given the version number. 4. We implement
> this by registering the marker interface objects as named utilities.
>
> Does this make sense?
Yes, this does make sense. And I do think you should be registering the marker
interface themselve. Remember that a class (or an interface) is an object in
itself. It's already a pattern in zope, the ZCA registers all interfaces as
utility already.
You are also right that what I suggest doesn't work, since the
IWebServiceRequest subclass don't provide IWebServiceRequest themselves. So
what you are doing is the proper way to do. I think I just don't like the name
(it's too long, and the utility you are retrieving isn't an implementation.)
How about IWebServiceVersion ?
>
> I believe in all these cases we have a specific request object whose
> .version we look up. If there was some way of extracting from the request
> object the marker interface that identified the version, we could write a
> property method WebServiceClien
> meant? (I could write this property method now, with the utility lookup,
> and it would clean up the code a bit.)
>
--
Francis J. Lacoste
<email address hidden>
Leonard Richardson (leonardr) wrote : | # |
> > === modified file 'src/lazr/
> > class EntryFieldURL(
> > """An IAbsoluteURL adapter for EntryField objects."""
> > component.
> > @@ -1243,7 +1246,7 @@
> > def __init__(self, context, request):
> > """Associate this resource with a specific object and request."""
> > super(EntryReso
> > - self.entry = IEntry(context)
> > + self.entry = getMultiAdapter
> >
>
> Not that if context is already providing IEntry, you'll get an error. I don't
> know if this is a plausible case, but if it is, you'll want to only query the
> adapter, if context doesn't provide IEntry.
I could go either way on this, but I kind of consider it an error to pass an Entry object directly into EntryResource. I think allowing that would encourage sloppy thinking and cause lazr.restful developers to confuse entries and data model objects. What do you think?
> > + # Create and register marker interfaces for request objects.
> > + for version in set(
> > + utility.
> > + classname = ("IWebServiceCl
> > + version.
>
> There are probably other characters that need to be squashed '-' is likely to
> be used.
I was lazy. I've added a make_identifier
> > === modified file 'src/lazr/
> > +URL generation
> > +--------------
> > +
> > + >>> from zope.component import getSiteManager
> > + >>> from zope.traversing
> > + >>> from lazr.restful.
> > + ... IRequestAwareLo
> > + >>> from lazr.restful.simple import AbsoluteURL
> > + >>> sm = getSiteManager()
> > + >>> sm.registerAdapter(
> > + ... AbsoluteURL, (IRequestAwareL
> > + ... provided=
>
> Can you add a paragraph explaining what the above does?
OK.
> > Here's the interface for the 'set' object that manages the contacts.
> >
> > + >>> from zope.interface import implements
> > >>> from lazr.restful.
> > >>> class IContactSet(
> > - ... def getAll(self):
> > + ... def getAllContacts():
> > ... "Get all contacts."
> > ...
> > - ... def get(self, name):
> > + ... def get(request, name):
> > ... "Retrieve a single contact by name."
>
> Why is request part of the interface signature?
That method doesn't need to be defined in IContactSet at all, because it's inherited from ITraverseWithGet. ITraverseWithGe
> > Here's a simple ContactSet with a predefined list of contacts.
> >
> > + >>> from zope.publisher.
Leonard Richardson (leonardr) wrote : | # |
> You are also right that what I suggest doesn't work, since the
> IWebServiceRequest subclass don't provide IWebServiceRequest themselves. So
> what you are doing is the proper way to do. I think I just don't like the name
> (it's too long, and the utility you are retrieving isn't an implementation.)
> How about IWebServiceVersion ?
OK, I've renamed it to IWebServiceVersion.
Leonard Richardson (leonardr) wrote : | # |
Apropos getting rid of IRequestAwareLo
>>> class ContactSetLocat
... """An ILocation implementation for ContactSet objects."""
... implements(
...
... def __init__(self, context, request):
... self.context = contact_set
... self.request = request
...
... def __parent__(self, request):
... return getUtility(
...
... def __name__(self, request):
... if request.version == 'beta':
... return 'contact_list'
... return 'contacts'
>>> from zope.traversing
>>> sm.registerAdapter(
... ZopeAbsoluteURL, (ILocation, IWebServiceLayer),
... provided=
>>> sm.registerAdapter(
... ContactSetLocation, (IContactSet, IWebServiceLayer),
... provided=ILocation)
Unfortunately this doesn't work with Zope's AbsoluteURL implementation, which contains this line:
context = ILocation(context)
I never get a chance to convert ContactSet into a ContactSetLocation.
I can modify my alternate implementation of AbsoluteURL to try a multi-adapter lookup before trying a simple cast to ILocation. I'll still need the alternate AbsoluteURL but I'd be able to get rid of the IRequestAwareLo
Francis J. Lacoste (flacoste) wrote : | # |
On January 11, 2010, Leonard Richardson wrote:
> > > === modified file 'src/lazr/
> > > class EntryFieldURL(
> > > """An IAbsoluteURL adapter for EntryField objects."""
> > > component.
> > > @@ -1243,7 +1246,7 @@
> > > def __init__(self, context, request):
> > > """Associate this resource with a specific object and
> > > request.""" super(EntryReso
> > > self.entry = IEntry(context)
> > > + self.entry = getMultiAdapter
> >
> > Not that if context is already providing IEntry, you'll get an error. I
> > don't know if this is a plausible case, but if it is, you'll want to only
> > query the adapter, if context doesn't provide IEntry.
>
> I could go either way on this, but I kind of consider it an error to pass
> an Entry object directly into EntryResource. I think allowing that would
> encourage sloppy thinking and cause lazr.restful developers to confuse
> entries and data model objects. What do you think?
Sure.
> > > Here's a simple ContactSet with a predefined list of contacts.
> > >
> > > + >>> from zope.publisher.
> > > + >>> from lazr.restful.
> > >
> > > >>> from lazr.restful.simple import TraverseWithGet
> > >
> > > - >>> from zope.publisher.
> > >
> > > >>> class ContactSet(
> > >
> > > ... implements(
> > > - ... path = "contacts"
> > > ...
> > > ... def __init__(self):
> > > ... self.contacts = CONTACTS
> > > ...
> > > - ... def get(self, name):
> > > - ... contacts = [contact for contacts in self.contacts
> > > - ... if pair.name == name]
> > > + ... def get(self, request, name):
> > > + ... contacts = [contact for contact in self.contacts
> > > + ... if contact.name == name]
> > > ... if len(contacts) == 1:
> > > ... return contacts[0]
> > > ... return None
> >
> > From the implementation, I don't see why request is required?
>
> As before, it's inherited from ITraverseWithGet.
Ok.
--
Francis J. Lacoste
<email address hidden>
Francis J. Lacoste (flacoste) wrote : | # |
On January 11, 2010, Leonard Richardson wrote:
> Apropos getting rid of IRequestAwareLo
some code like this:
> >>> class ContactSetLocat
>
> ... """An ILocation implementation for ContactSet objects."""
> ... implements(
> ...
> ... def __init__(self, context, request):
> ... self.context = contact_set
> ... self.request = request
> ...
> ... def __parent__(self, request):
> ... return getUtility(
> name=request.
> ... def __name__(self, request):
> ... if request.version == 'beta':
> ... return 'contact_list'
> ... return 'contacts'
>
I'd expect __parent__ and __name__ to be property. You don't need the request
parameter since you have it from the constructor.
> >>> from zope.traversing
> >>> sm.registerAdapter(
>
> ... ZopeAbsoluteURL, (ILocation, IWebServiceLayer),
> ... provided=
>
> >>> sm.registerAdapter(
>
> ... ContactSetLocation, (IContactSet, IWebServiceLayer),
> ... provided=ILocation)
>
> Unfortunately this doesn't work with Zope's AbsoluteURL implementation,
> which contains this line:
>
> context = ILocation(context)
>
> I never get a chance to convert ContactSet into a ContactSetLocation.
I see. I suggest then that you simply provide an ILocation adapter and use
get_current_
>
> I can modify my alternate implementation of AbsoluteURL to try a
> multi-adapter lookup before trying a simple cast to ILocation. I'll still
> need the alternate AbsoluteURL but I'd be able to get rid of the
> IRequestAwareLo
> something?
>
--
Francis J. Lacoste
<email address hidden>
Leonard Richardson (leonardr) wrote : | # |
If I use get_current_
On that note, I remember what I was doing with the IRequestAwareLo
We don't really need that to get a basic system working, but it might be useful in the future. What do you think? I'll work on taking the code out for now.
Francis J. Lacoste (flacoste) wrote : | # |
On January 12, 2010, Leonard Richardson wrote:
> If I use get_current_
> adapter at all. (Though one is still possible.) I might as well make the
> data model objects implement ILocation directly.
>
> On that note, I remember what I was doing with the IRequestAwareLo
> thing. We can always implement a standard ILocation adapter that grabs the
> request and branches based on request.version. But with
> IRequestAwareLo
> different versions. BetaContactLocation can provide the location for
> version 'beta' and DevContactLocation can provide the location for version
> 'dev'.
>
> We don't really need that to get a basic system working, but it might be
> useful in the future. What do you think? I'll work on taking the code out
> for now.
>
We shouldn't introduce other abstraction layers until they are needed :-) I
call YAGNI for now.
--
Francis J. Lacoste
<email address hidden>
Leonard Richardson (leonardr) wrote : | # |
OK, I've made all the changes you requested. The complete diff of my changes since your review is here:
Francis J. Lacoste (flacoste) wrote : | # |
On January 12, 2010, Leonard Richardson wrote:
> OK, I've made all the changes you requested. The complete diff of my
> changes since your review is here:
>
> http://
>
That's great, thanks. My only comment is about the NEWS.txt file:
> === modified file 'src/lazr/
> --- src/lazr/
> +++ src/lazr/
> @@ -9,10 +9,11 @@
> changes. You *must* change your configuration object to get your code
> to work in this version! See "active_versions" below.
>
> -Added the precursor of a versioning system for web services. Clients
> -can now request the "trunk" of a web service as well as one published
> -version. Apart from the URIs served, the two web services are exactly
> -the same.
> +Added a versioning system for web services. Clients can now request
> +the "trunk" of a web service as well as one published version. Apart
> +from the URIs served, the two web services are exactly the same. There
> +is no way to serve two different versions of a web service without
> +defining both versions from scratch.
You might want to make this less harsh by stating that a next version will add
annotations to make that easy to do.
review approve code
status approved
--
Francis J. Lacoste
<email address hidden>
- 95. By Leonard Richardson
-
[r=flacoste] It's now possible to define two distinct web services based on the same data model.
Preview Diff
1 | === modified file 'src/lazr/restful/NEWS.txt' |
2 | --- src/lazr/restful/NEWS.txt 2009-11-16 14:49:53 +0000 |
3 | +++ src/lazr/restful/NEWS.txt 2010-01-12 15:21:22 +0000 |
4 | @@ -9,10 +9,11 @@ |
5 | changes. You *must* change your configuration object to get your code |
6 | to work in this version! See "active_versions" below. |
7 | |
8 | -Added the precursor of a versioning system for web services. Clients |
9 | -can now request the "trunk" of a web service as well as one published |
10 | -version. Apart from the URIs served, the two web services are exactly |
11 | -the same. |
12 | +Added a versioning system for web services. Clients can now request |
13 | +the "trunk" of a web service as well as one published version. Apart |
14 | +from the URIs served, the two web services are exactly the same. There |
15 | +is no way to serve two different versions of a web service without |
16 | +defining both versions from scratch. |
17 | |
18 | This release introduces a new field to IWebServiceConfiguration: |
19 | latest_version_uri_prefix. If you are rolling your own |
20 | |
21 | === modified file 'src/lazr/restful/_operation.py' |
22 | --- src/lazr/restful/_operation.py 2009-03-31 17:58:53 +0000 |
23 | +++ src/lazr/restful/_operation.py 2010-01-12 15:21:22 +0000 |
24 | @@ -4,7 +4,7 @@ |
25 | |
26 | import simplejson |
27 | |
28 | -from zope.component import getMultiAdapter, queryAdapter |
29 | +from zope.component import getMultiAdapter, queryMultiAdapter |
30 | from zope.event import notify |
31 | from zope.interface import Attribute, implements, providedBy |
32 | from zope.interface.interfaces import IInterface |
33 | @@ -80,11 +80,11 @@ |
34 | # this object served to the client. |
35 | return result |
36 | |
37 | - if queryAdapter(result, ICollection): |
38 | + if queryMultiAdapter((result, self.request), ICollection): |
39 | # If the result is a web service collection, serve only one |
40 | # batch of the collection. |
41 | - result = CollectionResource( |
42 | - ICollection(result), self.request).batch() |
43 | + collection = getMultiAdapter((result, self.request), ICollection) |
44 | + result = CollectionResource(collection, self.request).batch() |
45 | elif self.should_batch(result): |
46 | result = self.batch(result, self.request) |
47 | |
48 | @@ -93,7 +93,7 @@ |
49 | try: |
50 | json_representation = simplejson.dumps( |
51 | result, cls=ResourceJSONEncoder) |
52 | - except TypeError: |
53 | + except TypeError, e: |
54 | raise TypeError("Could not serialize object %s to JSON." % |
55 | result) |
56 | |
57 | |
58 | === modified file 'src/lazr/restful/_resource.py' |
59 | --- src/lazr/restful/_resource.py 2009-10-27 17:45:53 +0000 |
60 | +++ src/lazr/restful/_resource.py 2010-01-12 15:21:22 +0000 |
61 | @@ -18,6 +18,7 @@ |
62 | 'JSONItem', |
63 | 'ReadOnlyResource', |
64 | 'RedirectResource', |
65 | + 'register_versioned_request_utility', |
66 | 'render_field_to_html', |
67 | 'ResourceJSONEncoder', |
68 | 'RESTUtilityBase', |
69 | @@ -47,13 +48,15 @@ |
70 | from zope.app.pagetemplate.engine import TrustedAppPT |
71 | from zope import component |
72 | from zope.component import ( |
73 | - adapts, getAdapters, getAllUtilitiesRegisteredFor, getMultiAdapter, |
74 | - getUtility, queryAdapter, getGlobalSiteManager) |
75 | + adapts, getAdapters, getAllUtilitiesRegisteredFor, |
76 | + getGlobalSiteManager, getMultiAdapter, getSiteManager, getUtility, |
77 | + queryMultiAdapter) |
78 | from zope.component.interfaces import ComponentLookupError |
79 | from zope.event import notify |
80 | from zope.publisher.http import init_status_codes, status_reasons |
81 | from zope.interface import ( |
82 | - implementer, implements, implementedBy, providedBy, Interface) |
83 | + alsoProvides, implementer, implements, implementedBy, providedBy, |
84 | + Interface) |
85 | from zope.interface.common.sequence import IFiniteSequence |
86 | from zope.interface.interfaces import IInterface |
87 | from zope.location.interfaces import ILocation |
88 | @@ -82,7 +85,8 @@ |
89 | IResourceDELETEOperation, IResourceGETOperation, IResourcePOSTOperation, |
90 | IScopedCollection, IServiceRootResource, ITopLevelEntryLink, |
91 | IUnmarshallingDoesntNeedValue, IWebServiceClientRequest, |
92 | - IWebServiceConfiguration, LAZR_WEBSERVICE_NAME) |
93 | + IWebServiceConfiguration, IWebServiceLayer, IWebServiceVersion, |
94 | + LAZR_WEBSERVICE_NAME) |
95 | from lazr.restful.utils import get_current_browser_request |
96 | |
97 | |
98 | @@ -112,6 +116,17 @@ |
99 | return unicode(value) |
100 | |
101 | |
102 | +def register_versioned_request_utility(interface, version): |
103 | + """Registers a marker interface as a utility for version lookup. |
104 | + |
105 | + This function registers the given interface class as the |
106 | + IWebServiceVersion utility for the given version string. |
107 | + """ |
108 | + alsoProvides(interface, IWebServiceVersion) |
109 | + getSiteManager().registerUtility( |
110 | + interface, IWebServiceVersion, name=version) |
111 | + |
112 | + |
113 | class LazrPageTemplateFile(TrustedAppPT, PageTemplateFile): |
114 | "A page template class for generating web service-related documents." |
115 | pass |
116 | @@ -143,8 +158,9 @@ |
117 | return tuple(obj) |
118 | if isinstance(underlying_object, dict): |
119 | return dict(obj) |
120 | - if queryAdapter(obj, IEntry): |
121 | - obj = EntryResource(obj, get_current_browser_request()) |
122 | + request = get_current_browser_request() |
123 | + if queryMultiAdapter((obj, request), IEntry): |
124 | + obj = EntryResource(obj, request) |
125 | |
126 | return IJSONPublishable(obj).toDataForJSON() |
127 | |
128 | @@ -1220,6 +1236,7 @@ |
129 | self.__parent__ = self.entry.context |
130 | self.__name__ = self.name |
131 | |
132 | + |
133 | class EntryFieldURL(AbsoluteURL): |
134 | """An IAbsoluteURL adapter for EntryField objects.""" |
135 | component.adapts(EntryField, IHTTPRequest) |
136 | @@ -1243,7 +1260,7 @@ |
137 | def __init__(self, context, request): |
138 | """Associate this resource with a specific object and request.""" |
139 | super(EntryResource, self).__init__(context, request) |
140 | - self.entry = IEntry(context) |
141 | + self.entry = getMultiAdapter((context, request), IEntry) |
142 | |
143 | def _getETagCore(self, unmarshalled_field_values=None): |
144 | """Calculate the ETag for an entry. |
145 | @@ -1442,7 +1459,10 @@ |
146 | def __init__(self, context, request): |
147 | """Associate this resource with a specific object and request.""" |
148 | super(CollectionResource, self).__init__(context, request) |
149 | - self.collection = ICollection(context) |
150 | + if ICollection.providedBy(context): |
151 | + self.collection = context |
152 | + else: |
153 | + self.collection = getMultiAdapter((context, request), ICollection) |
154 | |
155 | def do_GET(self): |
156 | """Fetch a collection and render it as JSON.""" |
157 | @@ -1492,12 +1512,14 @@ |
158 | # Scoped collection. The type URL depends on what type of |
159 | # entry the collection holds. |
160 | schema = self.context.relationship.value_type.schema |
161 | - adapter = EntryAdapterUtility.forSchemaInterface(schema) |
162 | + adapter = EntryAdapterUtility.forSchemaInterface( |
163 | + schema, self.request) |
164 | return adapter.entry_page_type_link |
165 | else: |
166 | # Top-level collection. |
167 | schema = self.collection.entry_schema |
168 | - adapter = EntryAdapterUtility.forEntryInterface(schema) |
169 | + adapter = EntryAdapterUtility.forEntryInterface( |
170 | + schema, self.request) |
171 | return adapter.collection_type_link |
172 | |
173 | |
174 | @@ -1588,7 +1610,7 @@ |
175 | # class's singular or plural names. |
176 | schema = registration.required[0] |
177 | adapter = EntryAdapterUtility.forSchemaInterface( |
178 | - schema) |
179 | + schema, self.request) |
180 | |
181 | singular = adapter.singular_type |
182 | assert not singular_names.has_key(singular), ( |
183 | @@ -1662,7 +1684,7 @@ |
184 | # It's not a top-level resource. |
185 | continue |
186 | adapter = EntryAdapterUtility.forEntryInterface( |
187 | - entry_schema) |
188 | + entry_schema, self.request) |
189 | link_name = ("%s_collection_link" % adapter.plural_type) |
190 | top_level_resources[link_name] = utility |
191 | # Now, collect the top-level entries. |
192 | @@ -1687,26 +1709,28 @@ |
193 | """An individual entry.""" |
194 | implements(IEntry) |
195 | |
196 | - def __init__(self, context): |
197 | + def __init__(self, context, request): |
198 | """Associate the entry with some database model object.""" |
199 | self.context = context |
200 | + self.request = request |
201 | |
202 | |
203 | class Collection: |
204 | """A collection of entries.""" |
205 | implements(ICollection) |
206 | |
207 | - def __init__(self, context): |
208 | + def __init__(self, context, request): |
209 | """Associate the entry with some database model object.""" |
210 | self.context = context |
211 | + self.request = request |
212 | |
213 | |
214 | class ScopedCollection: |
215 | """A collection associated with some parent object.""" |
216 | implements(IScopedCollection) |
217 | - adapts(Interface, Interface) |
218 | + adapts(Interface, Interface, IWebServiceLayer) |
219 | |
220 | - def __init__(self, context, collection): |
221 | + def __init__(self, context, collection, request): |
222 | """Initialize the scoped collection. |
223 | |
224 | :param context: The object to which the collection is scoped. |
225 | @@ -1714,6 +1738,7 @@ |
226 | """ |
227 | self.context = context |
228 | self.collection = collection |
229 | + self.request = request |
230 | # Unknown at this time. Should be set by our call-site. |
231 | self.relationship = None |
232 | |
233 | @@ -1723,8 +1748,11 @@ |
234 | # We are given a model schema (IFoo). Look up the |
235 | # corresponding entry schema (IFooEntry). |
236 | model_schema = self.relationship.value_type.schema |
237 | - return getGlobalSiteManager().adapters.lookup1( |
238 | - model_schema, IEntry).schema |
239 | + request_interface = getUtility( |
240 | + IWebServiceVersion, |
241 | + name=self.request.version) |
242 | + return getGlobalSiteManager().adapters.lookup( |
243 | + (model_schema, request_interface), IEntry).schema |
244 | |
245 | def find(self): |
246 | """See `ICollection`.""" |
247 | @@ -1748,33 +1776,42 @@ |
248 | """ |
249 | |
250 | @classmethod |
251 | - def forSchemaInterface(cls, entry_interface): |
252 | + def forSchemaInterface(cls, entry_interface, request): |
253 | """Create an entry adapter utility, given a schema interface. |
254 | |
255 | A schema interface is one that can be annotated to produce a |
256 | subclass of IEntry. |
257 | """ |
258 | + request_interface = getUtility( |
259 | + IWebServiceVersion, name=request.version) |
260 | entry_class = getGlobalSiteManager().adapters.lookup( |
261 | - (entry_interface,), IEntry) |
262 | + (entry_interface, request_interface), IEntry) |
263 | assert entry_class is not None, ( |
264 | - "No IEntry adapter found for %s." % entry_interface.__name__) |
265 | + ("No IEntry adapter found for %s (web service version: %s)." |
266 | + % (entry_interface.__name__, request.version))) |
267 | return EntryAdapterUtility(entry_class) |
268 | |
269 | @classmethod |
270 | - def forEntryInterface(cls, entry_interface): |
271 | + def forEntryInterface(cls, entry_interface, request): |
272 | """Create an entry adapter utility, given a subclass of IEntry.""" |
273 | registrations = getGlobalSiteManager().registeredAdapters() |
274 | + # There should be one IEntry subclass registered for every |
275 | + # version of the web service. We'll go through the appropriate |
276 | + # IEntry registrations looking for one associated with the |
277 | + # same IWebServiceVersion interface we find on the 'request' |
278 | + # object. |
279 | entry_classes = [ |
280 | registration.factory for registration in registrations |
281 | if (IInterface.providedBy(registration.provided) |
282 | and registration.provided.isOrExtends(IEntry) |
283 | - and entry_interface.implementedBy(registration.factory))] |
284 | + and entry_interface.implementedBy(registration.factory) |
285 | + and registration.required[1].providedBy(request))] |
286 | assert not len(entry_classes) > 1, ( |
287 | - "%s provides more than one IEntry subclass." % |
288 | - entry_interface.__name__) |
289 | + "%s provides more than one IEntry subclass for version %s." % |
290 | + entry_interface.__name__, request.version) |
291 | assert not len(entry_classes) < 1, ( |
292 | - "%s does not provide any IEntry subclass." % |
293 | - entry_interface.__name__) |
294 | + "%s does not provide any IEntry subclass for version %s." % |
295 | + entry_interface.__name__, request.version) |
296 | return EntryAdapterUtility(entry_classes[0]) |
297 | |
298 | def __init__(self, entry_class): |
299 | |
300 | === modified file 'src/lazr/restful/declarations.py' |
301 | --- src/lazr/restful/declarations.py 2009-08-14 16:11:34 +0000 |
302 | +++ src/lazr/restful/declarations.py 2010-01-12 15:21:22 +0000 |
303 | @@ -51,12 +51,12 @@ |
304 | from lazr.restful.interface import copy_field |
305 | from lazr.restful.interfaces import ( |
306 | ICollection, IEntry, IResourceDELETEOperation, IResourceGETOperation, |
307 | - IResourcePOSTOperation, IWebServiceConfiguration, LAZR_WEBSERVICE_NAME, |
308 | - LAZR_WEBSERVICE_NS) |
309 | + IResourcePOSTOperation, IWebServiceConfiguration, IWebServiceVersion, |
310 | + LAZR_WEBSERVICE_NAME, LAZR_WEBSERVICE_NS) |
311 | from lazr.restful import ( |
312 | Collection, Entry, EntryAdapterUtility, ResourceOperation, ObjectLink) |
313 | from lazr.restful.security import protect_schema |
314 | -from lazr.restful.utils import camelcase_to_underscore_separated |
315 | +from lazr.restful.utils import camelcase_to_underscore_separated, get_current_browser_request |
316 | |
317 | LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS |
318 | COLLECTION_TYPE = 'collection' |
319 | @@ -780,8 +780,14 @@ |
320 | |
321 | def __get__(self, instance, owner): |
322 | """Look up the entry schema that adapts the model schema.""" |
323 | - entry_class = getGlobalSiteManager().adapters.lookup1( |
324 | - self.model_schema, IEntry) |
325 | + if instance is None or instance.request is None: |
326 | + request = get_current_browser_request() |
327 | + else: |
328 | + request = instance.request |
329 | + request_interface = getUtility( |
330 | + IWebServiceVersion, name=request.version) |
331 | + entry_class = getGlobalSiteManager().adapters.lookup( |
332 | + (self.model_schema, request_interface), IEntry) |
333 | if entry_class is None: |
334 | return None |
335 | return EntryAdapterUtility(entry_class).entry_interface |
336 | |
337 | === modified file 'src/lazr/restful/directives/__init__.py' |
338 | --- src/lazr/restful/directives/__init__.py 2009-11-19 15:33:49 +0000 |
339 | +++ src/lazr/restful/directives/__init__.py 2010-01-12 15:21:22 +0000 |
340 | @@ -10,15 +10,20 @@ |
341 | import martian |
342 | from zope.component import getSiteManager, getUtility |
343 | from zope.location.interfaces import ILocation |
344 | +from zope.interface import alsoProvides |
345 | +from zope.interface.interface import InterfaceClass |
346 | from zope.traversing.browser import AbsoluteURL |
347 | from zope.traversing.browser.interfaces import IAbsoluteURL |
348 | |
349 | from lazr.restful.interfaces import ( |
350 | - IServiceRootResource, IWebServiceConfiguration, IWebServiceLayer) |
351 | -from lazr.restful import ServiceRootResource |
352 | + IServiceRootResource, IWebServiceClientRequest, IWebServiceConfiguration, |
353 | + IWebServiceLayer, IWebServiceVersion) |
354 | +from lazr.restful import ( |
355 | + register_versioned_request_utility, ServiceRootResource) |
356 | from lazr.restful.simple import ( |
357 | BaseWebServiceConfiguration, Publication, Request, |
358 | RootResourceAbsoluteURL) |
359 | +from lazr.restful.utils import make_identifier_safe |
360 | |
361 | |
362 | class request_class(martian.Directive): |
363 | @@ -57,6 +62,10 @@ |
364 | |
365 | This grokker then registers an instance of your subclass as the |
366 | singleton configuration object. |
367 | + |
368 | + This grokker also creates marker interfaces for every web service |
369 | + version defined in the configuration, and registers each as an |
370 | + IWebServiceVersion utility. |
371 | """ |
372 | martian.component(BaseWebServiceConfiguration) |
373 | martian.directive(request_class) |
374 | @@ -84,7 +93,18 @@ |
375 | cls.get_request_user = get_request_user |
376 | |
377 | # Register as utility. |
378 | - getSiteManager().registerUtility(cls(), IWebServiceConfiguration) |
379 | + utility = cls() |
380 | + sm = getSiteManager() |
381 | + sm.registerUtility(utility, IWebServiceConfiguration) |
382 | + |
383 | + # Create and register marker interfaces for request objects. |
384 | + for version in set( |
385 | + utility.active_versions + [utility.latest_version_uri_prefix]): |
386 | + classname = ("IWebServiceClientRequestVersion" + |
387 | + make_identifier_safe(version)) |
388 | + marker_interface = InterfaceClass( |
389 | + classname, (IWebServiceClientRequest,), {}) |
390 | + register_versioned_request_utility(marker_interface, version) |
391 | return True |
392 | |
393 | |
394 | |
395 | === modified file 'src/lazr/restful/docs/multiversion.txt' |
396 | --- src/lazr/restful/docs/multiversion.txt 2009-11-19 16:43:08 +0000 |
397 | +++ src/lazr/restful/docs/multiversion.txt 2010-01-12 15:21:22 +0000 |
398 | @@ -5,10 +5,121 @@ |
399 | services. Typically these different services represent successive |
400 | versions of a single web service, improved over time. |
401 | |
402 | +This test defines three different versions of a web service ('beta', |
403 | +'1.0', and 'dev'), all based on the same underlying data model. |
404 | + |
405 | +Setup |
406 | +===== |
407 | + |
408 | +First, let's set up the web service infrastructure. Doing this first |
409 | +will let us create HTTP requests for different versions of the web |
410 | +service. The first step is to install the common ZCML used by all |
411 | +lazr.restful web services. |
412 | + |
413 | + >>> from zope.configuration import xmlconfig |
414 | + >>> zcmlcontext = xmlconfig.string(""" |
415 | + ... <configure xmlns="http://namespaces.zope.org/zope"> |
416 | + ... <include package="lazr.restful" file="basic-site.zcml"/> |
417 | + ... <utility |
418 | + ... factory="lazr.restful.example.base.filemanager.FileManager" /> |
419 | + ... </configure> |
420 | + ... """) |
421 | + |
422 | +Web service configuration object |
423 | +-------------------------------- |
424 | + |
425 | +Here's the web service configuration, which defines the three |
426 | +versions: 'beta', '1.0', and 'dev'. |
427 | + |
428 | + >>> from lazr.restful import directives |
429 | + >>> from lazr.restful.interfaces import IWebServiceConfiguration |
430 | + >>> from lazr.restful.simple import BaseWebServiceConfiguration |
431 | + >>> from lazr.restful.testing.webservice import WebServiceTestPublication |
432 | + |
433 | + >>> class WebServiceConfiguration(BaseWebServiceConfiguration): |
434 | + ... hostname = 'api.multiversion.dev' |
435 | + ... use_https = False |
436 | + ... active_versions = ['beta', '1.0'] |
437 | + ... latest_version_uri_prefix = 'dev' |
438 | + ... code_revision = 'test' |
439 | + ... max_batch_size = 100 |
440 | + ... view_permission = None |
441 | + ... directives.publication_class(WebServiceTestPublication) |
442 | + |
443 | + >>> from grokcore.component.testing import grok_component |
444 | + >>> ignore = grok_component( |
445 | + ... 'WebServiceConfiguration', WebServiceConfiguration) |
446 | + |
447 | + >>> from zope.component import getUtility |
448 | + >>> config = getUtility(IWebServiceConfiguration) |
449 | + |
450 | +URL generation |
451 | +-------------- |
452 | + |
453 | +The URL to an entry or collection is different in different versions |
454 | +of web service. Not only does every URL includes the version number as |
455 | +a path element ("http://api.multiversion.dev/1.0/..."), the name or |
456 | +location of an object might change from one version to another. |
457 | + |
458 | +We implement this in this example web service by defining ILocation |
459 | +implementations that retrieve the current browser request and branch |
460 | +based on the value of request.version. You'll see this in the |
461 | +ContactSet class. |
462 | + |
463 | +Here, we tell Zope to use Zope's default AbsoluteURL class for |
464 | +generating the URLs of objects that implement ILocation. There's no |
465 | +multiversion-specific code here. |
466 | + |
467 | + >>> from zope.component import getSiteManager |
468 | + >>> from zope.traversing.browser import AbsoluteURL |
469 | + >>> from zope.traversing.browser.interfaces import IAbsoluteURL |
470 | + >>> from zope.location.interfaces import ILocation |
471 | + >>> from lazr.restful.interfaces import IWebServiceLayer |
472 | + |
473 | + >>> sm = getSiteManager() |
474 | + >>> sm.registerAdapter( |
475 | + ... AbsoluteURL, (ILocation, IWebServiceLayer), |
476 | + ... provided=IAbsoluteURL) |
477 | + |
478 | +Defining the request marker interfaces |
479 | +-------------------------------------- |
480 | + |
481 | +Every version must have a corresponding subclass of |
482 | +IWebServiceClientRequest. Each interface class is registered as a |
483 | +named utility implementing IWebServiceVersion. For instance, in the |
484 | +example below, the IWebServiceRequest10 class will be registered as |
485 | +the IWebServiceVersion utility with the name "1.0". |
486 | + |
487 | +When a request comes in, lazr.restful figures out which version the |
488 | +client is asking for, and tags the request with the appropriate marker |
489 | +interface. The utility registrations make it easy to get the marker |
490 | +interface for a version, given the version string. |
491 | + |
492 | +In a real application, these interfaces will be generated and |
493 | +registered automatically. |
494 | + |
495 | + >>> from lazr.restful.interfaces import IWebServiceClientRequest |
496 | + >>> class IWebServiceRequestBeta(IWebServiceClientRequest): |
497 | + ... pass |
498 | + |
499 | + >>> class IWebServiceRequest10(IWebServiceClientRequest): |
500 | + ... pass |
501 | + |
502 | + >>> class IWebServiceRequestDev(IWebServiceClientRequest): |
503 | + ... pass |
504 | + |
505 | + >>> versions = ((IWebServiceRequestBeta, 'beta'), |
506 | + ... (IWebServiceRequest10, '1.0'), |
507 | + ... (IWebServiceRequestDev, 'dev')) |
508 | + |
509 | + >>> from lazr.restful import register_versioned_request_utility |
510 | + >>> for cls, version in versions: |
511 | + ... register_versioned_request_utility(cls, version) |
512 | + |
513 | Example model objects |
514 | ===================== |
515 | |
516 | -First let's define the data model. The model in webservice.txt is |
517 | +Now let's define the data model. The model in webservice.txt is |
518 | pretty complicated; this model will be just complicated enough to |
519 | illustrate how to publish multiple versions of a web service. |
520 | |
521 | @@ -18,40 +129,21 @@ |
522 | >>> from zope.interface import Interface, Attribute |
523 | >>> from zope.schema import Bool, Bytes, Int, Text, TextLine, Object |
524 | |
525 | - >>> class ITestDataObject(Interface): |
526 | - ... """A marker interface for data objects.""" |
527 | - ... path = Attribute("The path portion of this object's URL. " |
528 | - ... "Defined here for simplicity of testing.") |
529 | - |
530 | - >>> class IContact(ITestDataObject): |
531 | + >>> class IContact(Interface): |
532 | ... name = TextLine(title=u"Name", required=True) |
533 | ... phone = TextLine(title=u"Phone number", required=True) |
534 | ... fax = TextLine(title=u"Fax number", required=False) |
535 | |
536 | -Here's the interface for the 'set' object that manages the contacts. |
537 | +Here's an interface for the 'set' object that manages the |
538 | +contacts. |
539 | |
540 | >>> from lazr.restful.interfaces import ITraverseWithGet |
541 | - >>> class IContactSet(ITestDataObject, ITraverseWithGet): |
542 | - ... def getAll(self): |
543 | + >>> class IContactSet(ITraverseWithGet): |
544 | + ... def getAllContacts(): |
545 | ... "Get all contacts." |
546 | ... |
547 | - ... def get(self, name): |
548 | - ... "Retrieve a single contact by name." |
549 | - |
550 | -Before we can define any classes, a bit of web service setup. Let's |
551 | -make all component lookups use the global site manager. |
552 | - |
553 | - >>> from zope.component import getSiteManager |
554 | - >>> sm = getSiteManager() |
555 | - |
556 | - >>> from zope.component import adapter |
557 | - >>> from zope.component.interfaces import IComponentLookup |
558 | - >>> from zope.interface import implementer, Interface |
559 | - >>> @implementer(IComponentLookup) |
560 | - ... @adapter(Interface) |
561 | - ... def everything_uses_the_global_site_manager(context): |
562 | - ... return sm |
563 | - >>> sm.registerAdapter(everything_uses_the_global_site_manager) |
564 | + ... def findContacts(self, string, search_fax): |
565 | + ... """Find contacts by name, phone number, or fax number.""" |
566 | |
567 | Here's a simple implementation of IContact. |
568 | |
569 | @@ -59,40 +151,64 @@ |
570 | >>> from zope.interface import implements |
571 | >>> from lazr.restful.security import protect_schema |
572 | >>> class Contact: |
573 | - ... implements(IContact) |
574 | + ... implements(IContact, ILocation) |
575 | ... def __init__(self, name, phone, fax): |
576 | ... self.name = name |
577 | ... self.phone = phone |
578 | ... self.fax = fax |
579 | ... |
580 | ... @property |
581 | - ... def path(self): |
582 | - ... return 'contacts/' + quote(self.name) |
583 | + ... def __parent__(self): |
584 | + ... return ContactSet() |
585 | + ... |
586 | + ... @property |
587 | + ... def __name__(self): |
588 | + ... return self.name |
589 | >>> protect_schema(Contact, IContact) |
590 | |
591 | Here's a simple ContactSet with a predefined list of contacts. |
592 | |
593 | + >>> from zope.publisher.interfaces.browser import IBrowserRequest |
594 | + >>> from lazr.restful.interfaces import IServiceRootResource |
595 | >>> from lazr.restful.simple import TraverseWithGet |
596 | - >>> from zope.publisher.interfaces.browser import IBrowserRequest |
597 | + >>> from lazr.restful.utils import get_current_browser_request |
598 | >>> class ContactSet(TraverseWithGet): |
599 | - ... implements(IContactSet) |
600 | - ... path = "contacts" |
601 | + ... implements(IContactSet, ILocation) |
602 | ... |
603 | ... def __init__(self): |
604 | ... self.contacts = CONTACTS |
605 | ... |
606 | - ... def get(self, name): |
607 | - ... contacts = [contact for contacts in self.contacts |
608 | - ... if pair.name == name] |
609 | + ... def get(self, request, name): |
610 | + ... contacts = [contact for contact in self.contacts |
611 | + ... if contact.name == name] |
612 | ... if len(contacts) == 1: |
613 | ... return contacts[0] |
614 | ... return None |
615 | ... |
616 | ... def getAllContacts(self): |
617 | ... return self.contacts |
618 | + ... |
619 | + ... def findContacts(self, string, search_fax=True): |
620 | + ... return [contact for contact in self.contacts |
621 | + ... if (string in contact.name |
622 | + ... or string in contact.phone |
623 | + ... or (search_fax and string in contact.fax))] |
624 | + ... |
625 | + ... @property |
626 | + ... def __parent__(self): |
627 | + ... request = get_current_browser_request() |
628 | + ... return getUtility( |
629 | + ... IServiceRootResource, name=request.version) |
630 | + ... |
631 | + ... @property |
632 | + ... def __name__(self): |
633 | + ... request = get_current_browser_request() |
634 | + ... if request.version == 'beta': |
635 | + ... return 'contact_list' |
636 | + ... return 'contacts' |
637 | |
638 | - >>> sm.registerAdapter( |
639 | - ... TraverseWithGet, [ITestDataObject, IBrowserRequest]) |
640 | + >>> from lazr.restful.security import protect_schema |
641 | + >>> protect_schema(ContactSet, IContactSet) |
642 | |
643 | Here are the "model objects" themselves: |
644 | |
645 | @@ -118,7 +234,7 @@ |
646 | ... """Marker for a contact published through the web service.""" |
647 | |
648 | >>> from zope.interface import taggedValue |
649 | - >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME |
650 | + >>> from lazr.restful.interfaces import LAZR_WEBSERVICE_NAME |
651 | >>> class IContactEntryBeta(IContactEntry, IContact): |
652 | ... """The part of an author we expose through the web service.""" |
653 | ... taggedValue(LAZR_WEBSERVICE_NAME, |
654 | @@ -169,14 +285,18 @@ |
655 | ... implements(IContactEntryBeta) |
656 | ... delegates(IContactEntryBeta) |
657 | ... schema = IContactEntryBeta |
658 | + ... def __init__(self, context, request): |
659 | + ... self.context = context |
660 | + |
661 | >>> sm.registerAdapter( |
662 | - ... ContactEntryBeta, provided=IContactEntry, name="beta") |
663 | + ... ContactEntryBeta, [IContact, IWebServiceRequestBeta], |
664 | + ... provided=IContactEntry) |
665 | |
666 | By wrapping one of our predefined Contacts in a ContactEntryBeta |
667 | object, we can verify that it implements IContactEntryBeta and |
668 | IContactEntry. |
669 | |
670 | - >>> entry = ContactEntryBeta(C1) |
671 | + >>> entry = ContactEntryBeta(C1, None) |
672 | >>> IContactEntry.validateInvariants(entry) |
673 | >>> IContactEntryBeta.validateInvariants(entry) |
674 | |
675 | @@ -184,24 +304,26 @@ |
676 | properties to implement the different field names. |
677 | |
678 | >>> class ContactEntry10(Entry): |
679 | - ... adapts(IContact) |
680 | - ... implements(IContactEntry10) |
681 | - ... schema = IContactEntry10 |
682 | - ... |
683 | - ... def __init__(self, contact): |
684 | - ... self.contact = contact |
685 | - ... |
686 | - ... @property |
687 | - ... def phone_number(self): |
688 | - ... return self.contact.phone |
689 | - ... |
690 | - ... @property |
691 | - ... def fax_number(self): |
692 | - ... return self.contact.fax |
693 | + ... adapts(IContact) |
694 | + ... implements(IContactEntry10) |
695 | + ... delegates(IContactEntry10) |
696 | + ... schema = IContactEntry10 |
697 | + ... |
698 | + ... def __init__(self, context, request): |
699 | + ... self.context = context |
700 | + ... |
701 | + ... @property |
702 | + ... def phone_number(self): |
703 | + ... return self.context.phone |
704 | + ... |
705 | + ... @property |
706 | + ... def fax_number(self): |
707 | + ... return self.context.fax |
708 | >>> sm.registerAdapter( |
709 | - ... ContactEntry10, provided=IContactEntry, name="1.0") |
710 | + ... ContactEntry10, [IContact, IWebServiceRequest10], |
711 | + ... provided=IContactEntry) |
712 | |
713 | - >>> entry = ContactEntry10(C1) |
714 | + >>> entry = ContactEntry10(C1, None) |
715 | >>> IContactEntry.validateInvariants(entry) |
716 | >>> IContactEntry10.validateInvariants(entry) |
717 | |
718 | @@ -209,20 +331,22 @@ |
719 | the web service. |
720 | |
721 | >>> class ContactEntryDev(Entry): |
722 | - ... adapts(IContact) |
723 | - ... implements(IContactEntryDev) |
724 | - ... schema = IContactEntryDev |
725 | - ... |
726 | - ... def __init__(self, contact): |
727 | - ... self.contact = contact |
728 | - ... |
729 | - ... @property |
730 | - ... def phone_number(self): |
731 | - ... return self.contact.phone |
732 | + ... adapts(IContact) |
733 | + ... implements(IContactEntryDev) |
734 | + ... delegates(IContactEntryDev) |
735 | + ... schema = IContactEntryDev |
736 | + ... |
737 | + ... def __init__(self, context, request): |
738 | + ... self.context = context |
739 | + ... |
740 | + ... @property |
741 | + ... def phone_number(self): |
742 | + ... return self.context.phone |
743 | >>> sm.registerAdapter( |
744 | - ... ContactEntryDev, provided=IContactEntry, name="dev") |
745 | + ... ContactEntryDev, [IContact, IWebServiceRequestDev], |
746 | + ... provided=IContactEntry) |
747 | |
748 | - >>> entry = ContactEntryDev(C1) |
749 | + >>> entry = ContactEntryDev(C1, None) |
750 | >>> IContactEntry.validateInvariants(entry) |
751 | >>> IContactEntryDev.validateInvariants(entry) |
752 | |
753 | @@ -239,19 +363,35 @@ |
754 | ... |
755 | ComponentLookupError: ... |
756 | |
757 | -When adapting Contact to IEntry you must specify a version number as |
758 | -the name of the adapter. The object you get back will implement the |
759 | -appropriate version of the web service. |
760 | - |
761 | - >>> beta_entry = getAdapter(C1, IEntry, name="beta") |
762 | +When adapting Contact to IEntry you must provide a versioned request |
763 | +object. The IEntry object you get back will implement the appropriate |
764 | +version of the web service. |
765 | + |
766 | +To test this we'll need to manually create some versioned request |
767 | +objects. The traversal process would take care of this for us (see |
768 | +"Request lifecycle" below), but it won't work yet because we have yet |
769 | +to define a service root resource. |
770 | + |
771 | + >>> from lazr.restful.testing.webservice import ( |
772 | + ... create_web_service_request) |
773 | + >>> from zope.interface import alsoProvides |
774 | + |
775 | + >>> from zope.component import getMultiAdapter |
776 | + >>> request_beta = create_web_service_request('/beta/') |
777 | + >>> alsoProvides(request_beta, IWebServiceRequestBeta) |
778 | + >>> beta_entry = getMultiAdapter((C1, request_beta), IEntry) |
779 | >>> print beta_entry.fax |
780 | 111-2121 |
781 | |
782 | - >>> one_oh_entry = getAdapter(C1, IEntry, name="1.0") |
783 | + >>> request_10 = create_web_service_request('/1.0/') |
784 | + >>> alsoProvides(request_10, IWebServiceRequest10) |
785 | + >>> one_oh_entry = getMultiAdapter((C1, request_10), IEntry) |
786 | >>> print one_oh_entry.fax_number |
787 | 111-2121 |
788 | |
789 | - >>> dev_entry = getAdapter(C1, IEntry, name="dev") |
790 | + >>> request_dev = create_web_service_request('/dev/') |
791 | + >>> alsoProvides(request_dev, IWebServiceRequestDev) |
792 | + >>> dev_entry = getMultiAdapter((C1, request_dev), IEntry) |
793 | >>> print dev_entry.fax |
794 | Traceback (most recent call last): |
795 | ... |
796 | @@ -260,9 +400,13 @@ |
797 | Implementing the collection resource |
798 | ==================================== |
799 | |
800 | -The contact collection itself doesn't change between versions (though |
801 | -it could). We'll define it once and register it for every version of |
802 | -the web service. |
803 | +The set of contacts publishes a slightly different named operation in |
804 | +every version of the web service, so in a little bit we'll be |
805 | +implementing three different versions of the same named operation. But |
806 | +the contact set itself doesn't change between versions (although it |
807 | +could). So it's sufficient to implement one ICollection implementation |
808 | +and register it as the implementation for every version of the web |
809 | +service. |
810 | |
811 | >>> from lazr.restful import Collection |
812 | >>> from lazr.restful.interfaces import ICollection |
813 | @@ -277,67 +421,87 @@ |
814 | ... """Find all the contacts.""" |
815 | ... return self.context.getAllContacts() |
816 | |
817 | -Let's make sure ContactCollection implements ICollection. |
818 | +Let's make sure it implements ICollection. |
819 | |
820 | >>> from zope.interface.verify import verifyObject |
821 | >>> contact_set = ContactSet() |
822 | - >>> verifyObject(ICollection, ContactCollection(contact_set)) |
823 | + >>> verifyObject(ICollection, ContactCollection(contact_set, None)) |
824 | True |
825 | |
826 | -Once we register ContactCollection as the ICollection implementation, |
827 | -we can adapt the ContactSet object to a web service ICollection. |
828 | - |
829 | - >>> for version in ['beta', 'dev', '1.0']: |
830 | - ... sm.registerAdapter( |
831 | - ... ContactCollection, provided=ICollection, name=version) |
832 | - |
833 | - >>> dev_collection = getAdapter(contact_set, ICollection, name="dev") |
834 | - >>> len(dev_collection.find()) |
835 | - 2 |
836 | - |
837 | - >>> dev_collection = getAdapter(contact_set, ICollection, name="dev") |
838 | - >>> len(dev_collection.find()) |
839 | - 2 |
840 | - |
841 | -Web service infrastructure initialization |
842 | -========================================= |
843 | - |
844 | -Now that we've defined the data model, it's time to set up the web |
845 | -service infrastructure. |
846 | - |
847 | - >>> from zope.configuration import xmlconfig |
848 | - >>> zcmlcontext = xmlconfig.string(""" |
849 | - ... <configure xmlns="http://namespaces.zope.org/zope"> |
850 | - ... <include package="lazr.restful" file="basic-site.zcml"/> |
851 | - ... <utility |
852 | - ... factory="lazr.restful.example.base.filemanager.FileManager" /> |
853 | - ... </configure> |
854 | - ... """) |
855 | - |
856 | -Here's the configuration, which defines the three versions: 'beta', |
857 | -'1.0', and 'dev'. |
858 | - |
859 | - >>> from lazr.restful import directives |
860 | - >>> from lazr.restful.interfaces import IWebServiceConfiguration |
861 | - >>> from lazr.restful.simple import BaseWebServiceConfiguration |
862 | - >>> from lazr.restful.testing.webservice import WebServiceTestPublication |
863 | - |
864 | - >>> class WebServiceConfiguration(BaseWebServiceConfiguration): |
865 | - ... hostname = 'api.multiversion.dev' |
866 | - ... use_https = False |
867 | - ... active_versions = ['beta', '1.0'] |
868 | - ... latest_version_uri_prefix = 'dev' |
869 | - ... code_revision = 'test' |
870 | - ... max_batch_size = 100 |
871 | - ... directives.publication_class(WebServiceTestPublication) |
872 | - |
873 | - >>> from grokcore.component.testing import grok_component |
874 | - >>> ignore = grok_component( |
875 | - ... 'WebServiceConfiguration', WebServiceConfiguration) |
876 | - |
877 | - >>> from zope.component import getUtility |
878 | - >>> config = getUtility(IWebServiceConfiguration) |
879 | - |
880 | +Register it as the ICollection adapter for IContactSet. We use a |
881 | +generic request interface (IWebServiceClientRequest) rather than a |
882 | +specific one like IWebServiceRequestBeta, so that the same |
883 | +implementation will be used for every version of the web service. |
884 | + |
885 | + >>> sm.registerAdapter( |
886 | + ... ContactCollection, [IContactSet, IWebServiceClientRequest], |
887 | + ... provided=ICollection) |
888 | + |
889 | +Make sure the functionality works properly. |
890 | + |
891 | + >>> collection = getMultiAdapter( |
892 | + ... (contact_set, request_beta), ICollection) |
893 | + >>> len(collection.find()) |
894 | + 2 |
895 | + |
896 | +Implementing the named operations |
897 | +--------------------------------- |
898 | + |
899 | +All three versions of the web service publish a named operation for |
900 | +searching for contacts, but they publish it in slightly different |
901 | +ways. In 'beta' it publishes a named operation called 'findContacts', |
902 | +which does a search based on name, phone number, and fax number. In |
903 | +'1.0' it publishes the same operation, but the name is |
904 | +'find'. In 'dev' the contact set publishes 'find', |
905 | +but the functionality is changed to search only the name and phone |
906 | +number. |
907 | + |
908 | +Here's the named operation as implemented in versions 'beta' and '1.0'. |
909 | + |
910 | + >>> from lazr.restful import ResourceGETOperation |
911 | + >>> from lazr.restful.fields import CollectionField, Reference |
912 | + >>> from lazr.restful.interfaces import IResourceGETOperation |
913 | + >>> class FindContactsOperationBase(ResourceGETOperation): |
914 | + ... """An operation that searches for contacts.""" |
915 | + ... implements(IResourceGETOperation) |
916 | + ... |
917 | + ... params = [ TextLine(__name__='string') ] |
918 | + ... return_type = CollectionField(value_type=Reference(schema=IContact)) |
919 | + ... |
920 | + ... def call(self, string): |
921 | + ... try: |
922 | + ... return self.context.findContacts(string) |
923 | + ... except ValueError, e: |
924 | + ... self.request.response.setStatus(400) |
925 | + ... return str(e) |
926 | + |
927 | +This operation is registered as the "findContacts" operation in the |
928 | +'beta' service, and the 'find' operation in the '1.0' service. |
929 | + |
930 | + >>> sm.registerAdapter( |
931 | + ... FindContactsOperationBase, [IContactSet, IWebServiceRequestBeta], |
932 | + ... provided=IResourceGETOperation, name="findContacts") |
933 | + |
934 | + >>> sm.registerAdapter( |
935 | + ... FindContactsOperationBase, [IContactSet, IWebServiceRequest10], |
936 | + ... provided=IResourceGETOperation, name="find") |
937 | + |
938 | +Here's the slightly different named operation as implemented in |
939 | +version 'dev'. |
940 | + |
941 | + >>> class FindContactsOperationNoFax(FindContactsOperationBase): |
942 | + ... """An operation that searches for contacts.""" |
943 | + ... |
944 | + ... def call(self, string): |
945 | + ... try: |
946 | + ... return self.context.findContacts(string, False) |
947 | + ... except ValueError, e: |
948 | + ... self.request.response.setStatus(400) |
949 | + ... return str(e) |
950 | + |
951 | + >>> sm.registerAdapter( |
952 | + ... FindContactsOperationNoFax, [IContactSet, IWebServiceRequestDev], |
953 | + ... provided=IResourceGETOperation, name="find") |
954 | |
955 | The service root resource |
956 | ========================= |
957 | @@ -346,19 +510,20 @@ |
958 | roots. The 'beta' web service will publish the contact set as |
959 | 'contact_list', and subsequent versions will publish it as 'contacts'. |
960 | |
961 | - >>> from lazr.restful.interfaces import IServiceRootResource |
962 | >>> from lazr.restful.simple import RootResource |
963 | >>> from zope.traversing.browser.interfaces import IAbsoluteURL |
964 | |
965 | >>> class BetaServiceRootResource(RootResource): |
966 | ... implements(IAbsoluteURL) |
967 | ... |
968 | - ... top_level_objects = { 'contact_list': ContactSet() } |
969 | + ... top_level_collections = { |
970 | + ... 'contact_list': (IContact, ContactSet()) } |
971 | |
972 | >>> class PostBetaServiceRootResource(RootResource): |
973 | ... implements(IAbsoluteURL) |
974 | ... |
975 | - ... top_level_objects = { 'contacts': ContactSet() } |
976 | + ... top_level_collections = { |
977 | + ... 'contacts': (IContact, ContactSet()) } |
978 | |
979 | >>> for version, cls in (('beta', BetaServiceRootResource), |
980 | ... ('1.0', PostBetaServiceRootResource), |
981 | @@ -378,22 +543,338 @@ |
982 | Both classes will use the default lazr.restful code to generate their |
983 | URLs. |
984 | |
985 | + >>> from zope.traversing.browser import absoluteURL |
986 | >>> from lazr.restful.simple import RootResourceAbsoluteURL |
987 | >>> for cls in (BetaServiceRootResource, PostBetaServiceRootResource): |
988 | ... sm.registerAdapter( |
989 | ... RootResourceAbsoluteURL, [cls, IBrowserRequest]) |
990 | |
991 | - >>> from zope.traversing.browser import absoluteURL |
992 | - >>> from lazr.restful.testing.webservice import ( |
993 | - ... create_web_service_request) |
994 | - |
995 | >>> beta_request = create_web_service_request('/beta/') |
996 | - >>> ignore = beta_request.traverse(None) |
997 | + >>> print beta_request.traverse(None) |
998 | + <BetaServiceRootResource object...> |
999 | + |
1000 | >>> print absoluteURL(beta_app, beta_request) |
1001 | http://api.multiversion.dev/beta/ |
1002 | |
1003 | >>> dev_request = create_web_service_request('/dev/') |
1004 | - >>> ignore = dev_request.traverse(None) |
1005 | + >>> print dev_request.traverse(None) |
1006 | + <PostBetaServiceRootResource object...> |
1007 | + |
1008 | >>> print absoluteURL(dev_app, dev_request) |
1009 | http://api.multiversion.dev/dev/ |
1010 | |
1011 | +Request lifecycle |
1012 | +================= |
1013 | + |
1014 | +When a request first comes in, there's no way to tell which version |
1015 | +it's associated with. |
1016 | + |
1017 | + >>> from lazr.restful.testing.webservice import ( |
1018 | + ... create_web_service_request) |
1019 | + |
1020 | + >>> request_beta = create_web_service_request('/beta/') |
1021 | + >>> IWebServiceRequestBeta.providedBy(request_beta) |
1022 | + False |
1023 | + |
1024 | +The traversal process associates the request with a particular version. |
1025 | + |
1026 | + >>> request_beta.traverse(None) |
1027 | + <BetaServiceRootResource object ...> |
1028 | + >>> IWebServiceRequestBeta.providedBy(request_beta) |
1029 | + True |
1030 | + >>> print request_beta.version |
1031 | + beta |
1032 | + |
1033 | +Using the web service |
1034 | +===================== |
1035 | + |
1036 | +Now that we can create versioned web service requests, let's try out |
1037 | +the different versions of the web service. |
1038 | + |
1039 | +Beta |
1040 | +---- |
1041 | + |
1042 | +Here's the service root resource. |
1043 | + |
1044 | + >>> import simplejson |
1045 | + >>> request = create_web_service_request('/beta/') |
1046 | + >>> resource = request.traverse(None) |
1047 | + >>> body = simplejson.loads(resource()) |
1048 | + >>> print sorted(body.keys()) |
1049 | + ['contacts_collection_link', 'resource_type_link'] |
1050 | + |
1051 | + >>> print body['contacts_collection_link'] |
1052 | + http://api.multiversion.dev/beta/contact_list |
1053 | + |
1054 | +Here's the contact list. |
1055 | + |
1056 | + >>> request = create_web_service_request('/beta/contact_list') |
1057 | + >>> resource = request.traverse(None) |
1058 | + |
1059 | +We can't access the underlying data model object through the request, |
1060 | +but since we happen to know which object it is, we can pass it into |
1061 | +absoluteURL along with the request object, and get the correct URL. |
1062 | + |
1063 | + >>> print absoluteURL(contact_set, request) |
1064 | + http://api.multiversion.dev/beta/contact_list |
1065 | + |
1066 | + >>> body = simplejson.loads(resource()) |
1067 | + >>> body['total_size'] |
1068 | + 2 |
1069 | + >>> for link in sorted( |
1070 | + ... [contact['self_link'] for contact in body['entries']]): |
1071 | + ... print link |
1072 | + http://api.multiversion.dev/beta/contact_list/Cleo%20Python |
1073 | + http://api.multiversion.dev/beta/contact_list/Oliver%20Bluth |
1074 | + |
1075 | +We can traverse through the collection to an entry. |
1076 | + |
1077 | + >>> request_beta = create_web_service_request( |
1078 | + ... '/beta/contact_list/Cleo Python') |
1079 | + >>> resource = request_beta.traverse(None) |
1080 | + |
1081 | +Again, we can't access the underlying data model object through the |
1082 | +request, but since we know which object represents Cleo Python, we can |
1083 | +pass it into absoluteURL along with this request object, and get the |
1084 | +object's URL. |
1085 | + |
1086 | + >>> print C1.name |
1087 | + Cleo Python |
1088 | + >>> print absoluteURL(C1, request_beta) |
1089 | + http://api.multiversion.dev/beta/contact_list/Cleo%20Python |
1090 | + |
1091 | + >>> body = simplejson.loads(resource()) |
1092 | + >>> sorted(body.keys()) |
1093 | + ['fax', 'http_etag', 'name', 'phone', 'resource_type_link', 'self_link'] |
1094 | + >>> print body['name'] |
1095 | + Cleo Python |
1096 | + |
1097 | +We can traverse through an entry to one of its fields. |
1098 | + |
1099 | + >>> request_beta = create_web_service_request( |
1100 | + ... '/beta/contact_list/Cleo Python/fax') |
1101 | + >>> field = request_beta.traverse(None) |
1102 | + >>> print simplejson.loads(field()) |
1103 | + 111-2121 |
1104 | + |
1105 | +We can invoke a named operation. |
1106 | + |
1107 | + >>> import simplejson |
1108 | + >>> request_beta = create_web_service_request( |
1109 | + ... '/beta/contact_list', |
1110 | + ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=Cleo'}) |
1111 | + >>> operation = request_beta.traverse(None) |
1112 | + >>> result = simplejson.loads(operation()) |
1113 | + >>> [contact['name'] for contact in result['entries']] |
1114 | + ['Cleo Python'] |
1115 | + |
1116 | + >>> request_beta = create_web_service_request( |
1117 | + ... '/beta/contact_list', |
1118 | + ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=111'}) |
1119 | + |
1120 | + >>> operation = request_beta.traverse(None) |
1121 | + >>> result = simplejson.loads(operation()) |
1122 | + >>> [contact['fax'] for contact in result['entries']] |
1123 | + ['111-2121'] |
1124 | + |
1125 | +1.0 |
1126 | +--- |
1127 | + |
1128 | +Here's the service root resource. |
1129 | + |
1130 | + >>> import simplejson |
1131 | + >>> request = create_web_service_request('/1.0/') |
1132 | + >>> resource = request.traverse(None) |
1133 | + >>> body = simplejson.loads(resource()) |
1134 | + >>> print sorted(body.keys()) |
1135 | + ['contacts_collection_link', 'resource_type_link'] |
1136 | + |
1137 | +Note that 'contacts_collection_link' points to a different URL in |
1138 | +'1.0' than in 'dev'. |
1139 | + |
1140 | + >>> print body['contacts_collection_link'] |
1141 | + http://api.multiversion.dev/1.0/contacts |
1142 | + |
1143 | +An attempt to use the 'beta' name of the contact list in the '1.0' web |
1144 | +service will fail. |
1145 | + |
1146 | + >>> request = create_web_service_request('/1.0/contact_list') |
1147 | + >>> resource = request.traverse(None) |
1148 | + Traceback (most recent call last): |
1149 | + ... |
1150 | + NotFound: Object: <PostBetaServiceRootResource...>, name: u'contact_list' |
1151 | + |
1152 | +Here's the contact list under its correct URL. |
1153 | + |
1154 | + >>> request = create_web_service_request('/1.0/contacts') |
1155 | + >>> resource = request.traverse(None) |
1156 | + >>> print absoluteURL(contact_set, request) |
1157 | + http://api.multiversion.dev/1.0/contacts |
1158 | + |
1159 | + >>> body = simplejson.loads(resource()) |
1160 | + >>> body['total_size'] |
1161 | + 2 |
1162 | + >>> for link in sorted( |
1163 | + ... [contact['self_link'] for contact in body['entries']]): |
1164 | + ... print link |
1165 | + http://api.multiversion.dev/1.0/contacts/Cleo%20Python |
1166 | + http://api.multiversion.dev/1.0/contacts/Oliver%20Bluth |
1167 | + |
1168 | +We can traverse through the collection to an entry. |
1169 | + |
1170 | + >>> request_10 = create_web_service_request( |
1171 | + ... '/1.0/contacts/Cleo Python') |
1172 | + >>> resource = request_10.traverse(None) |
1173 | + >>> print absoluteURL(C1, request_10) |
1174 | + http://api.multiversion.dev/1.0/contacts/Cleo%20Python |
1175 | + |
1176 | +Note that the 'fax' and 'phone' fields are now called 'fax_number' and |
1177 | +'phone_number'. |
1178 | + |
1179 | + >>> body = simplejson.loads(resource()) |
1180 | + >>> sorted(body.keys()) |
1181 | + ['fax_number', 'http_etag', 'name', 'phone_number', |
1182 | + 'resource_type_link', 'self_link'] |
1183 | + >>> print body['name'] |
1184 | + Cleo Python |
1185 | + |
1186 | +We can traverse through an entry to one of its fields. |
1187 | + |
1188 | + >>> request_10 = create_web_service_request( |
1189 | + ... '/1.0/contacts/Cleo Python/fax_number') |
1190 | + >>> field = request_10.traverse(None) |
1191 | + >>> print simplejson.loads(field()) |
1192 | + 111-2121 |
1193 | + |
1194 | +The fax field in '1.0' is called 'fax_number', and attempting |
1195 | +to traverse to its 'beta' name ('fax') will fail. |
1196 | + |
1197 | + >>> request_10 = create_web_service_request( |
1198 | + ... '/1.0/contacts/Cleo Python/fax') |
1199 | + >>> field = request_10.traverse(None) |
1200 | + Traceback (most recent call last): |
1201 | + ... |
1202 | + NotFound: Object: <Contact object...>, name: u'fax' |
1203 | + |
1204 | +We can invoke a named operation. Note that the name of the operation |
1205 | +is now 'find' (it was 'findContacts' in 'beta'). |
1206 | + |
1207 | + >>> request_10 = create_web_service_request( |
1208 | + ... '/1.0/contacts', |
1209 | + ... environ={'QUERY_STRING' : 'ws.op=find&string=Cleo'}) |
1210 | + >>> operation = request_10.traverse(None) |
1211 | + >>> result = simplejson.loads(operation()) |
1212 | + >>> [contact['name'] for contact in result['entries']] |
1213 | + ['Cleo Python'] |
1214 | + |
1215 | + >>> request_10 = create_web_service_request( |
1216 | + ... '/1.0/contacts', |
1217 | + ... environ={'QUERY_STRING' : 'ws.op=find&string=111'}) |
1218 | + >>> operation = request_10.traverse(None) |
1219 | + >>> result = simplejson.loads(operation()) |
1220 | + >>> [contact['fax_number'] for contact in result['entries']] |
1221 | + ['111-2121'] |
1222 | + |
1223 | +Attempting to invoke the operation using its 'beta' name won't work. |
1224 | + |
1225 | + >>> request_10 = create_web_service_request( |
1226 | + ... '/1.0/contacts', |
1227 | + ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=Cleo'}) |
1228 | + >>> operation = request_10.traverse(None) |
1229 | + >>> print operation() |
1230 | + No such operation: findContacts |
1231 | + |
1232 | +Dev |
1233 | +--- |
1234 | + |
1235 | +Here's the service root resource. |
1236 | + |
1237 | + >>> request = create_web_service_request('/dev/') |
1238 | + >>> resource = request.traverse(None) |
1239 | + >>> body = simplejson.loads(resource()) |
1240 | + >>> print sorted(body.keys()) |
1241 | + ['contacts_collection_link', 'resource_type_link'] |
1242 | + |
1243 | + >>> print body['contacts_collection_link'] |
1244 | + http://api.multiversion.dev/dev/contacts |
1245 | + |
1246 | +Here's the contact list. |
1247 | + |
1248 | + >>> request_dev = create_web_service_request('/dev/contacts') |
1249 | + >>> resource = request_dev.traverse(None) |
1250 | + >>> print absoluteURL(contact_set, request_dev) |
1251 | + http://api.multiversion.dev/dev/contacts |
1252 | + |
1253 | + >>> body = simplejson.loads(resource()) |
1254 | + >>> body['total_size'] |
1255 | + 2 |
1256 | + >>> for link in sorted( |
1257 | + ... [contact['self_link'] for contact in body['entries']]): |
1258 | + ... print link |
1259 | + http://api.multiversion.dev/dev/contacts/Cleo%20Python |
1260 | + http://api.multiversion.dev/dev/contacts/Oliver%20Bluth |
1261 | + |
1262 | +We can traverse through the collection to an entry. |
1263 | + |
1264 | + >>> request_dev = create_web_service_request( |
1265 | + ... '/dev/contacts/Cleo Python') |
1266 | + >>> resource = request_dev.traverse(None) |
1267 | + >>> print absoluteURL(C1, request_dev) |
1268 | + http://api.multiversion.dev/dev/contacts/Cleo%20Python |
1269 | + |
1270 | +Note that the published field names have changed between 'dev' and |
1271 | +'1.0'. The phone field is still 'phone_number', but the 'fax_number' |
1272 | +field is gone. |
1273 | + |
1274 | + >>> body = simplejson.loads(resource()) |
1275 | + >>> sorted(body.keys()) |
1276 | + ['http_etag', 'name', 'phone_number', 'resource_type_link', 'self_link'] |
1277 | + >>> print body['name'] |
1278 | + Cleo Python |
1279 | + |
1280 | +We can traverse through an entry to one of its fields. |
1281 | + |
1282 | + >>> request_dev = create_web_service_request( |
1283 | + ... '/dev/contacts/Cleo Python/name') |
1284 | + >>> field = request_dev.traverse(None) |
1285 | + >>> print simplejson.loads(field()) |
1286 | + Cleo Python |
1287 | + |
1288 | +We cannot use 'dev' to traverse to a field not published in the 'dev' |
1289 | +version. |
1290 | + |
1291 | + >>> request_beta = create_web_service_request( |
1292 | + ... '/dev/contacts/Cleo Python/fax') |
1293 | + >>> field = request_beta.traverse(None) |
1294 | + Traceback (most recent call last): |
1295 | + ... |
1296 | + NotFound: Object: <Contact object...>, name: u'fax' |
1297 | + |
1298 | + >>> request_beta = create_web_service_request( |
1299 | + ... '/dev/contacts/Cleo Python/fax_number') |
1300 | + >>> field = request_beta.traverse(None) |
1301 | + Traceback (most recent call last): |
1302 | + ... |
1303 | + NotFound: Object: <Contact object...>, name: u'fax_number' |
1304 | + |
1305 | +We can invoke a named operation. |
1306 | + |
1307 | + >>> request_dev = create_web_service_request( |
1308 | + ... '/dev/contacts', |
1309 | + ... environ={'QUERY_STRING' : 'ws.op=find&string=Cleo'}) |
1310 | + >>> operation = request_dev.traverse(None) |
1311 | + >>> result = simplejson.loads(operation()) |
1312 | + >>> [contact['name'] for contact in result['entries']] |
1313 | + ['Cleo Python'] |
1314 | + |
1315 | +Note that a search for Cleo's fax number no longer finds anything, |
1316 | +because the named operation published as 'find' in the 'dev' web |
1317 | +service doesn't search the fax field. |
1318 | + |
1319 | + >>> request_dev = create_web_service_request( |
1320 | + ... '/dev/contacts', |
1321 | + ... environ={'QUERY_STRING' : 'ws.op=find&string=111'}) |
1322 | + >>> operation = request_dev.traverse(None) |
1323 | + >>> result = simplejson.loads(operation()) |
1324 | + >>> result['total_size'] |
1325 | + 0 |
1326 | |
1327 | === modified file 'src/lazr/restful/docs/utils.txt' |
1328 | --- src/lazr/restful/docs/utils.txt 2009-08-27 15:50:55 +0000 |
1329 | +++ src/lazr/restful/docs/utils.txt 2010-01-12 15:21:22 +0000 |
1330 | @@ -75,8 +75,33 @@ |
1331 | >>> print implementation().a_method() |
1332 | superclass result |
1333 | |
1334 | - |
1335 | -================================= |
1336 | +make_identifier_safe |
1337 | +==================== |
1338 | + |
1339 | +LAZR provides a way of converting an arbitrary string into a similar |
1340 | +string that can be used as a Python identifier. |
1341 | + |
1342 | + >>> from lazr.restful.utils import make_identifier_safe |
1343 | + >>> print make_identifier_safe("already_a_valid_IDENTIFIER_444") |
1344 | + already_a_valid_IDENTIFIER_444 |
1345 | + |
1346 | + >>> print make_identifier_safe("!starts_with_punctuation") |
1347 | + _starts_with_punctuation |
1348 | + |
1349 | + >>> print make_identifier_safe("_!contains!pu-nc.tuation") |
1350 | + __contains_pu_nc_tuation |
1351 | + |
1352 | + >>> print make_identifier_safe("contains\nnewline") |
1353 | + contains_newline |
1354 | + |
1355 | + >>> print make_identifier_safe("") |
1356 | + _ |
1357 | + |
1358 | + >>> print make_identifier_safe(None) |
1359 | + Traceback (most recent call last): |
1360 | + ... |
1361 | + ValueError: Cannot make None value identifier-safe. |
1362 | + |
1363 | camelcase_to_underscore_separated |
1364 | ================================= |
1365 | |
1366 | @@ -97,7 +122,6 @@ |
1367 | >>> camelcase_to_underscore_separated('_StartsWithUnderscore') |
1368 | '__starts_with_underscore' |
1369 | |
1370 | -============== |
1371 | safe_hasattr() |
1372 | ============== |
1373 | |
1374 | @@ -130,7 +154,6 @@ |
1375 | >>> safe_hasattr(oracle, 'weather') |
1376 | False |
1377 | |
1378 | -============ |
1379 | smartquote() |
1380 | ============ |
1381 | |
1382 | @@ -155,7 +178,6 @@ |
1383 | >>> smartquote('a lot of "foo"?') |
1384 | u'a lot of \u201cfoo\u201d?' |
1385 | |
1386 | -================ |
1387 | safe_js_escape() |
1388 | ================ |
1389 | |
1390 | |
1391 | === modified file 'src/lazr/restful/docs/webservice-declarations.txt' |
1392 | --- src/lazr/restful/docs/webservice-declarations.txt 2009-11-16 14:49:53 +0000 |
1393 | +++ src/lazr/restful/docs/webservice-declarations.txt 2010-01-12 15:21:22 +0000 |
1394 | @@ -867,8 +867,50 @@ |
1395 | ... self.base_price = base_price |
1396 | ... self.inventory_number = inventory_number |
1397 | |
1398 | +Before we can continue, we must define a web service configuration |
1399 | +object. Each web service needs to have one of these registered |
1400 | +utilities providing basic information about the web service. This one |
1401 | +is just a dummy. |
1402 | + |
1403 | + >>> from zope.component import provideUtility |
1404 | + >>> from lazr.restful.interfaces import IWebServiceConfiguration |
1405 | + >>> class MyWebServiceConfiguration: |
1406 | + ... implements(IWebServiceConfiguration) |
1407 | + ... view_permission = "lazr.View" |
1408 | + ... active_versions = ["beta"] |
1409 | + ... code_revision = "1.0b" |
1410 | + ... default_batch_size = 50 |
1411 | + ... latest_version_uri_prefix = 'beta' |
1412 | + ... |
1413 | + ... def get_request_user(self): |
1414 | + ... return 'A user' |
1415 | + >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration) |
1416 | + |
1417 | + |
1418 | +We must also set up the ability to create versioned requests. We only |
1419 | +have one version of the web service ('beta'), but lazr.restful |
1420 | +requires every request to be marked with a version string and to |
1421 | +implement an appropriate marker interface. Here, we define the marker |
1422 | +interface for the 'beta' version of the web service. |
1423 | + |
1424 | + >>> from zope.component import getSiteManager |
1425 | + >>> from lazr.restful.interfaces import IWebServiceVersion |
1426 | + >>> class ITestServiceRequestBeta(IWebServiceVersion): |
1427 | + ... pass |
1428 | + >>> sm = getSiteManager() |
1429 | + >>> sm.registerUtility( |
1430 | + ... ITestServiceRequestBeta, IWebServiceVersion, |
1431 | + ... name='beta') |
1432 | + |
1433 | + >>> from lazr.restful.testing.webservice import FakeRequest |
1434 | + >>> request = FakeRequest() |
1435 | + |
1436 | +Now we can turn a Book object into something that implements |
1437 | +IBookEntry. |
1438 | + |
1439 | >>> entry_adapter = entry_adapter_factory( |
1440 | - ... Book(u'Aldous Huxley', u'Island', 10.0, '12345')) |
1441 | + ... Book(u'Aldous Huxley', u'Island', 10.0, '12345'), |
1442 | + ... request) |
1443 | |
1444 | >>> entry_adapter.schema is entry_interface |
1445 | True |
1446 | @@ -926,7 +968,7 @@ |
1447 | ... return self.books |
1448 | |
1449 | >>> collection_adapter = collection_adapter_factory( |
1450 | - ... BookSet(['A book', 'Another book'])) |
1451 | + ... BookSet(['A book', 'Another book']), request) |
1452 | |
1453 | >>> verifyObject(ICollection, collection_adapter) |
1454 | True |
1455 | @@ -943,24 +985,6 @@ |
1456 | find(). The REQUEST_USER marker value will be replaced by the logged in |
1457 | user. |
1458 | |
1459 | -To get this to work we must define a web service configuration |
1460 | -object. Each web service needs to have one of these registered |
1461 | -utilities providing basic information about the web service. This one |
1462 | -is just a dummy. |
1463 | - |
1464 | - >>> from zope.component import provideUtility |
1465 | - >>> from lazr.restful.interfaces import IWebServiceConfiguration |
1466 | - >>> class MyWebServiceConfiguration: |
1467 | - ... implements(IWebServiceConfiguration) |
1468 | - ... view_permission = "lazr.View" |
1469 | - ... active_versions = ["beta"] |
1470 | - ... code_revision = "1.0b" |
1471 | - ... default_batch_size = 50 |
1472 | - ... |
1473 | - ... def get_request_user(self): |
1474 | - ... return 'A user' |
1475 | - >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration) |
1476 | - |
1477 | >>> class CheckedOutBookSet(object): |
1478 | ... """Simple ICheckedOutBookSet implementation.""" |
1479 | ... implements(ICheckedOutBookSet) |
1480 | @@ -970,7 +994,7 @@ |
1481 | ... user, title) |
1482 | |
1483 | >>> checked_out_adapter = generate_collection_adapter( |
1484 | - ... ICheckedOutBookSet)(CheckedOutBookSet()) |
1485 | + ... ICheckedOutBookSet)(CheckedOutBookSet(), request) |
1486 | |
1487 | >>> checked_out_adapter.find() |
1488 | A user searched for checked out book matching "". |
1489 | @@ -1046,7 +1070,6 @@ |
1490 | |
1491 | Now we can create a fake request that invokes the named operation. |
1492 | |
1493 | - >>> from lazr.restful.testing.webservice import FakeRequest |
1494 | >>> request = FakeRequest() |
1495 | >>> read_method_adapter = read_method_adapter_factory( |
1496 | ... BookSetOnSteroids(), request) |
1497 | @@ -1298,7 +1321,7 @@ |
1498 | ... IHasText, hastext_entry_interface) |
1499 | |
1500 | >>> obj = HasText() |
1501 | - >>> hastext_entry_adapter = hastext_entry_adapter_factory(obj) |
1502 | + >>> hastext_entry_adapter = hastext_entry_adapter_factory(obj, request) |
1503 | |
1504 | ...and you'll have an object that invokes set_text() when you set the |
1505 | 'text' attribute. |
1506 | @@ -1531,11 +1554,11 @@ |
1507 | After the registration, adapters from IBook to IEntry, and IBookSet to |
1508 | ICollection are available: |
1509 | |
1510 | - >>> from zope.component import getAdapter |
1511 | + >>> from zope.component import getMultiAdapter |
1512 | >>> book = Book(u'George Orwell', u'1984', 10.0, u'12345-1984') |
1513 | >>> bookset = BookSet([book]) |
1514 | |
1515 | - >>> entry_adapter = getAdapter(book, IEntry) |
1516 | + >>> entry_adapter = getMultiAdapter((book, request), IEntry) |
1517 | >>> verifyObject(IEntry, entry_adapter) |
1518 | True |
1519 | |
1520 | @@ -1544,7 +1567,7 @@ |
1521 | >>> verifyObject(entry_adapter.schema, entry_adapter) |
1522 | True |
1523 | |
1524 | - >>> collection_adapter = getAdapter(bookset, ICollection) |
1525 | + >>> collection_adapter = getMultiAdapter((bookset, request), ICollection) |
1526 | >>> verifyObject(ICollection, collection_adapter) |
1527 | True |
1528 | |
1529 | |
1530 | === modified file 'src/lazr/restful/docs/webservice-error.txt' |
1531 | --- src/lazr/restful/docs/webservice-error.txt 2009-08-04 19:27:13 +0000 |
1532 | +++ src/lazr/restful/docs/webservice-error.txt 2010-01-12 15:21:22 +0000 |
1533 | @@ -15,6 +15,7 @@ |
1534 | >>> class SimpleWebServiceConfiguration: |
1535 | ... implements(IWebServiceConfiguration) |
1536 | ... show_tracebacks = False |
1537 | + ... latest_version_uri_prefix = 'trunk' |
1538 | >>> webservice_configuration = SimpleWebServiceConfiguration() |
1539 | >>> getSiteManager().registerUtility(webservice_configuration) |
1540 | |
1541 | |
1542 | === modified file 'src/lazr/restful/docs/webservice.txt' |
1543 | --- src/lazr/restful/docs/webservice.txt 2009-11-19 16:28:38 +0000 |
1544 | +++ src/lazr/restful/docs/webservice.txt 2010-01-12 15:21:22 +0000 |
1545 | @@ -114,23 +114,14 @@ |
1546 | |
1547 | >>> from urllib import quote |
1548 | >>> from zope.component import ( |
1549 | - ... adapts, adapter, getSiteManager, getMultiAdapter) |
1550 | - >>> from zope.interface import implements, implementer, Interface |
1551 | + ... adapts, getSiteManager, getMultiAdapter) |
1552 | + >>> from zope.interface import implements |
1553 | >>> from zope.publisher.interfaces import IPublishTraverse, NotFound |
1554 | >>> from zope.publisher.interfaces.browser import IBrowserRequest |
1555 | >>> from zope.security.checker import CheckerPublic |
1556 | >>> from zope.traversing.browser.interfaces import IAbsoluteURL |
1557 | >>> from lazr.restful.security import protect_schema |
1558 | |
1559 | - >>> from zope.component.interfaces import IComponentLookup |
1560 | - >>> sm = getSiteManager() |
1561 | - |
1562 | - >>> @implementer(IComponentLookup) |
1563 | - ... @adapter(Interface) |
1564 | - ... def everything_uses_the_global_site_manager(context): |
1565 | - ... return sm |
1566 | - >>> sm.registerAdapter(everything_uses_the_global_site_manager) |
1567 | - |
1568 | >>> class BaseAbsoluteURL: |
1569 | ... """A basic, extensible implementation of IAbsoluteURL.""" |
1570 | ... implements(IAbsoluteURL) |
1571 | @@ -143,6 +134,8 @@ |
1572 | ... return "http://api.cookbooks.dev/beta/" + self.context.path |
1573 | ... |
1574 | ... __call__ = __str__ |
1575 | + |
1576 | + >>> sm = getSiteManager() |
1577 | >>> sm.registerAdapter( |
1578 | ... BaseAbsoluteURL, [ITestDataObject, IBrowserRequest], |
1579 | ... IAbsoluteURL) |
1580 | @@ -520,6 +513,26 @@ |
1581 | >>> from zope.component import getUtility |
1582 | >>> webservice_configuration = getUtility(IWebServiceConfiguration) |
1583 | |
1584 | +We also need to define a marker interface for each version of the web |
1585 | +service, so that incoming requests can be marked with the appropriate |
1586 | +version string. The configuration above defines two versions, 'beta' |
1587 | +and 'devel'. |
1588 | + |
1589 | + >>> from lazr.restful.interfaces import IWebServiceClientRequest |
1590 | + >>> class IWebServiceRequestBeta(IWebServiceClientRequest): |
1591 | + ... pass |
1592 | + |
1593 | + >>> class IWebServiceRequestDevel(IWebServiceClientRequest): |
1594 | + ... pass |
1595 | + |
1596 | + >>> versions = ((IWebServiceRequestBeta, 'beta'), |
1597 | + ... (IWebServiceRequestDevel, 'devel')) |
1598 | + |
1599 | + >>> from lazr.restful import register_versioned_request_utility |
1600 | + >>> for cls, version in versions: |
1601 | + ... register_versioned_request_utility(cls, version) |
1602 | + |
1603 | + |
1604 | ====================== |
1605 | Defining the resources |
1606 | ====================== |
1607 | @@ -625,6 +638,7 @@ |
1608 | >>> from zope.interface.verify import verifyObject |
1609 | >>> from lazr.delegates import delegates |
1610 | >>> from lazr.restful import Entry |
1611 | + >>> from lazr.restful.testing.webservice import FakeRequest |
1612 | |
1613 | >>> class AuthorEntry(Entry): |
1614 | ... """An author, as exposed through the web service.""" |
1615 | @@ -632,7 +646,8 @@ |
1616 | ... delegates(IAuthorEntry) |
1617 | ... schema = IAuthorEntry |
1618 | |
1619 | - >>> verifyObject(IAuthorEntry, AuthorEntry(A1)) |
1620 | + >>> request = FakeRequest() |
1621 | + >>> verifyObject(IAuthorEntry, AuthorEntry(A1, request)) |
1622 | True |
1623 | |
1624 | The ``schema`` attribute points to the interface class that defines the |
1625 | @@ -643,18 +658,17 @@ |
1626 | the interface defined in the schema attribute. This is usually not a problem, |
1627 | since the schema is usually the interface itself. |
1628 | |
1629 | - >>> IAuthorEntry.validateInvariants(AuthorEntry(A1)) |
1630 | + >>> IAuthorEntry.validateInvariants(AuthorEntry(A1, request)) |
1631 | |
1632 | But the invariant will complain if that isn't true. |
1633 | |
1634 | >>> class InvalidAuthorEntry(Entry): |
1635 | - ... adapts(IAuthor) |
1636 | ... delegates(IAuthorEntry) |
1637 | ... schema = ICookbookEntry |
1638 | |
1639 | - >>> verifyObject(IAuthorEntry, InvalidAuthorEntry(A1)) |
1640 | + >>> verifyObject(IAuthorEntry, InvalidAuthorEntry(A1, request)) |
1641 | True |
1642 | - >>> IAuthorEntry.validateInvariants(InvalidAuthorEntry(A1)) |
1643 | + >>> IAuthorEntry.validateInvariants(InvalidAuthorEntry(A1, request)) |
1644 | Traceback (most recent call last): |
1645 | ... |
1646 | Invalid: InvalidAuthorEntry doesn't provide its ICookbookEntry schema. |
1647 | @@ -663,40 +677,43 @@ |
1648 | |
1649 | >>> class CookbookEntry(Entry): |
1650 | ... """A cookbook, as exposed through the web service.""" |
1651 | - ... adapts(ICookbook) |
1652 | ... delegates(ICookbookEntry) |
1653 | ... schema = ICookbookEntry |
1654 | |
1655 | >>> class DishEntry(Entry): |
1656 | ... """A dish, as exposed through the web service.""" |
1657 | - ... adapts(IDish) |
1658 | ... delegates(IDishEntry) |
1659 | ... schema = IDishEntry |
1660 | |
1661 | >>> class CommentEntry(Entry): |
1662 | ... """A comment, as exposed through the web service.""" |
1663 | - ... adapts(IComment) |
1664 | ... delegates(ICommentEntry) |
1665 | ... schema = ICommentEntry |
1666 | |
1667 | >>> class RecipeEntry(Entry): |
1668 | - ... adapts(IRecipe) |
1669 | ... delegates(IRecipeEntry) |
1670 | ... schema = IRecipeEntry |
1671 | |
1672 | -We need to register these entries as an adapter from (e.g.) ``IAuthor`` to |
1673 | -(e.g.) ``IAuthorEntry``. In ZCML a registration would look like this. |
1674 | +We need to register these entries as a multiadapter adapter from |
1675 | +(e.g.) ``IAuthor`` and ``IWebServiceClientRequest`` to (e.g.) |
1676 | +``IAuthorEntry``. In ZCML a registration would look like this. |
1677 | |
1678 | - <adapter factory="my.app.rest.AuthorEntry" /> |
1679 | + <adapter for="my.app.rest.IAuthor |
1680 | + lazr.restful.interfaces.IWebServiceClientRequest" |
1681 | + factory="my.app.rest.AuthorEntry" /> |
1682 | |
1683 | Since we're in the middle of a Python example we can do the equivalent |
1684 | -in Python code: |
1685 | +in Python code for each entry class: |
1686 | |
1687 | - >>> sm.registerAdapter(AuthorEntry, provided=IAuthorEntry) |
1688 | - >>> sm.registerAdapter(CookbookEntry, provided=ICookbookEntry) |
1689 | - >>> sm.registerAdapter(DishEntry, provided=IDishEntry) |
1690 | - >>> sm.registerAdapter(CommentEntry, provided=ICommentEntry) |
1691 | - >>> sm.registerAdapter(RecipeEntry, provided=IRecipeEntry) |
1692 | + >>> for entry_class, adapts_interface, provided_interface in [ |
1693 | + ... [AuthorEntry, IAuthor, IAuthorEntry], |
1694 | + ... [CookbookEntry, ICookbook, ICookbookEntry], |
1695 | + ... [DishEntry, IDish, IDishEntry], |
1696 | + ... [CommentEntry, IComment, ICommentEntry], |
1697 | + ... [RecipeEntry, IRecipe, IRecipeEntry]]: |
1698 | + ... sm.registerAdapter( |
1699 | + ... entry_class, [adapts_interface, IWebServiceClientRequest], |
1700 | + ... provided=provided_interface) |
1701 | |
1702 | lazr.restful also defines an interface and a base class for collections of |
1703 | objects. I'll use it to expose the ``AuthorSet`` collection and other |
1704 | @@ -708,7 +725,6 @@ |
1705 | |
1706 | >>> class AuthorCollection(Collection): |
1707 | ... """A collection of authors, as exposed through the web service.""" |
1708 | - ... adapts(IAuthorSet) |
1709 | ... |
1710 | ... entry_schema = IAuthorEntry |
1711 | ... |
1712 | @@ -716,9 +732,11 @@ |
1713 | ... """Find all the authors.""" |
1714 | ... return self.context.getAllAuthors() |
1715 | |
1716 | - >>> sm.registerAdapter(AuthorCollection) |
1717 | + >>> sm.registerAdapter(AuthorCollection, |
1718 | + ... (IAuthorSet, IWebServiceClientRequest), |
1719 | + ... provided=ICollection) |
1720 | |
1721 | - >>> verifyObject(ICollection, AuthorCollection(AuthorSet())) |
1722 | + >>> verifyObject(ICollection, AuthorCollection(AuthorSet(), request)) |
1723 | True |
1724 | |
1725 | >>> class CookbookCollection(Collection): |
1726 | @@ -731,7 +749,9 @@ |
1727 | ... def find(self): |
1728 | ... """Find all the cookbooks.""" |
1729 | ... return self.context.getAll() |
1730 | - >>> sm.registerAdapter(CookbookCollection) |
1731 | + >>> sm.registerAdapter(CookbookCollection, |
1732 | + ... (ICookbookSet, IWebServiceClientRequest), |
1733 | + ... provided=ICollection) |
1734 | |
1735 | >>> class DishCollection(Collection): |
1736 | ... """A collection of dishes, as exposed through the web service.""" |
1737 | @@ -742,7 +762,10 @@ |
1738 | ... def find(self): |
1739 | ... """Find all the dishes.""" |
1740 | ... return self.context.getAll() |
1741 | - >>> sm.registerAdapter(DishCollection) |
1742 | + |
1743 | + >>> sm.registerAdapter(DishCollection, |
1744 | + ... (IDishSet, IWebServiceClientRequest), |
1745 | + ... provided=ICollection) |
1746 | |
1747 | Like ``Entry``, ``Collection`` is a simple base class that defines a |
1748 | constructor. The ``entry_schema`` attribute gives a ``Collection`` class |
1749 | @@ -762,8 +785,9 @@ |
1750 | |
1751 | >>> def scope_collection(parent, child, name): |
1752 | ... """A helper method that simulates a scoped collection lookup.""" |
1753 | - ... parent_entry = IEntry(parent) |
1754 | - ... scoped = getMultiAdapter((parent_entry, IEntry(child)), |
1755 | + ... parent_entry = getMultiAdapter((parent, request), IEntry) |
1756 | + ... child_entry = getMultiAdapter((child, request), IEntry) |
1757 | + ... scoped = getMultiAdapter((parent_entry, child_entry, request), |
1758 | ... IScopedCollection) |
1759 | ... scoped.relationship = parent_entry.schema.get(name) |
1760 | ... return scoped |
1761 | |
1762 | === modified file 'src/lazr/restful/example/base/root.py' |
1763 | --- src/lazr/restful/example/base/root.py 2009-11-12 19:08:10 +0000 |
1764 | +++ src/lazr/restful/example/base/root.py 2010-01-12 15:21:22 +0000 |
1765 | @@ -16,12 +16,11 @@ |
1766 | |
1767 | from zope.interface import implements |
1768 | from zope.location.interfaces import ILocation |
1769 | -from zope.component import adapts, getUtility |
1770 | +from zope.component import adapts, getMultiAdapter, getUtility |
1771 | from zope.schema.interfaces import IBytes |
1772 | |
1773 | -from lazr.restful import ServiceRootResource |
1774 | +from lazr.restful import directives, ServiceRootResource |
1775 | |
1776 | -from lazr.restful import directives |
1777 | from lazr.restful.interfaces import ( |
1778 | IByteStorage, IEntry, IServiceRootResource, ITopLevelEntryLink, |
1779 | IWebServiceConfiguration) |
1780 | @@ -30,6 +29,7 @@ |
1781 | IFileManager, IRecipe, IRecipeSet, IHasGet, NameAlreadyTaken) |
1782 | from lazr.restful.simple import BaseWebServiceConfiguration |
1783 | from lazr.restful.testing.webservice import WebServiceTestPublication |
1784 | +from lazr.restful.utils import get_current_browser_request |
1785 | |
1786 | |
1787 | #Entry classes. |
1788 | @@ -148,7 +148,8 @@ |
1789 | self.recipes.remove(recipe) |
1790 | |
1791 | def replace_cover(self, cover): |
1792 | - storage = SimpleByteStorage(IEntry(self), ICookbook['cover']) |
1793 | + entry = getMultiAdapter((self, get_current_browser_request()), IEntry) |
1794 | + storage = SimpleByteStorage(entry, ICookbook['cover']) |
1795 | storage.createStored('application/octet-stream', cover, 'cover') |
1796 | |
1797 | |
1798 | |
1799 | === modified file 'src/lazr/restful/interfaces/_rest.py' |
1800 | --- src/lazr/restful/interfaces/_rest.py 2009-11-18 17:33:20 +0000 |
1801 | +++ src/lazr/restful/interfaces/_rest.py 2010-01-12 15:21:22 +0000 |
1802 | @@ -49,6 +49,7 @@ |
1803 | 'LAZR_WEBSERVICE_NS', |
1804 | 'IWebServiceClientRequest', |
1805 | 'IWebServiceLayer', |
1806 | + 'IWebServiceVersion', |
1807 | ] |
1808 | |
1809 | from zope.schema import Bool, Int, List, TextLine |
1810 | @@ -251,13 +252,27 @@ |
1811 | |
1812 | |
1813 | class IWebServiceClientRequest(IBrowserRequest): |
1814 | - """Marker interface requests to the web service.""" |
1815 | + """Interface for requests to the web service.""" |
1816 | + version = Attribute("The version of the web service that the client " |
1817 | + "requested.") |
1818 | |
1819 | |
1820 | class IWebServiceLayer(IWebServiceClientRequest, IDefaultBrowserLayer): |
1821 | """Marker interface for registering views on the web service.""" |
1822 | |
1823 | |
1824 | +class IWebServiceVersion(Interface): |
1825 | + """Used to register IWebServiceClientRequest subclasses as utilities. |
1826 | + |
1827 | + Every version of a web service must register a subclass of |
1828 | + IWebServiceClientRequest as an IWebServiceVersion utility, with a |
1829 | + name that's the web service version name. For instance: |
1830 | + |
1831 | + registerUtility(IWebServiceClientRequestBeta, |
1832 | + IWebServiceVersion, name="beta") |
1833 | + """ |
1834 | + pass |
1835 | + |
1836 | class IJSONRequestCache(Interface): |
1837 | """A cache of objects exposed as URLs or JSON representations.""" |
1838 | |
1839 | |
1840 | === modified file 'src/lazr/restful/metazcml.py' |
1841 | --- src/lazr/restful/metazcml.py 2009-04-16 20:45:55 +0000 |
1842 | +++ src/lazr/restful/metazcml.py 2010-01-12 15:21:22 +0000 |
1843 | @@ -82,8 +82,14 @@ |
1844 | context.action( |
1845 | discriminator=('adapter', interface, provides, ''), |
1846 | callable=handler, |
1847 | + # XXX leonardr bug=503948 Register the adapter against a |
1848 | + # generic IWebServiceClientRequest. It will be picked up |
1849 | + # for all versions of the web service. Later on, this will |
1850 | + # be changed to register different adapters for different |
1851 | + # versions. |
1852 | args=('registerAdapter', |
1853 | - factory, (interface, ), provides, '', context.info), |
1854 | + factory, (interface, IWebServiceClientRequest), |
1855 | + provides, '', context.info), |
1856 | ) |
1857 | register_webservice_operations(context, interface) |
1858 | |
1859 | |
1860 | === modified file 'src/lazr/restful/publisher.py' |
1861 | --- src/lazr/restful/publisher.py 2009-11-19 15:53:26 +0000 |
1862 | +++ src/lazr/restful/publisher.py 2010-01-12 15:21:22 +0000 |
1863 | @@ -35,7 +35,7 @@ |
1864 | from lazr.restful.interfaces import ( |
1865 | IByteStorage, ICollection, ICollectionField, IEntry, IEntryField, |
1866 | IHTTPResource, IServiceRootResource, IWebBrowserInitiatedRequest, |
1867 | - IWebServiceClientRequest, IWebServiceConfiguration) |
1868 | + IWebServiceClientRequest, IWebServiceConfiguration, IWebServiceVersion) |
1869 | |
1870 | |
1871 | class WebServicePublicationMixin: |
1872 | @@ -57,8 +57,10 @@ |
1873 | # handle traversing to the scoped collection itself. |
1874 | if len(request.getTraversalStack()) == 0: |
1875 | try: |
1876 | - entry = IEntry(ob) |
1877 | - except TypeError: |
1878 | + entry = getMultiAdapter((ob, request), IEntry) |
1879 | + except ComponentLookupError: |
1880 | + # This doesn't look like a lazr.restful object. Let |
1881 | + # the superclass handle traversal. |
1882 | pass |
1883 | else: |
1884 | if name.endswith("_link"): |
1885 | @@ -111,7 +113,7 @@ |
1886 | collection = getattr(entry, name, None) |
1887 | if collection is None: |
1888 | return None |
1889 | - scoped_collection = ScopedCollection(entry.context, entry) |
1890 | + scoped_collection = ScopedCollection(entry.context, entry, request) |
1891 | # Tell the IScopedCollection object what collection it's managing, |
1892 | # and what the collection's relationship is to the entry it's |
1893 | # scoped to. |
1894 | @@ -138,11 +140,11 @@ |
1895 | appropriate resource. |
1896 | """ |
1897 | if (ICollection.providedBy(ob) or |
1898 | - queryAdapter(ob, ICollection) is not None): |
1899 | + queryMultiAdapter((ob, request), ICollection) is not None): |
1900 | # Object supports ICollection protocol. |
1901 | resource = CollectionResource(ob, request) |
1902 | elif (IEntry.providedBy(ob) or |
1903 | - queryAdapter(ob, IEntry) is not None): |
1904 | + queryMultiAdapter((ob, request), IEntry) is not None): |
1905 | # Object supports IEntry protocol. |
1906 | resource = EntryResource(ob, request) |
1907 | elif (IEntryField.providedBy(ob) or |
1908 | @@ -255,11 +257,17 @@ |
1909 | raise NotFound(self, '', self) |
1910 | self.annotations[self.VERSION_ANNOTATION] = version |
1911 | |
1912 | + # Find the version-specific interface this request should |
1913 | + # provide, and provide it. |
1914 | + to_provide = getUtility(IWebServiceVersion, name=version) |
1915 | + alsoProvides(self, to_provide) |
1916 | + self.version = version |
1917 | + |
1918 | # Find the appropriate service root for this version and set |
1919 | # the publication's application appropriately. |
1920 | try: |
1921 | # First, try to find a version-specific service root. |
1922 | - service_root = getUtility(IServiceRootResource, name=version) |
1923 | + service_root = getUtility(IServiceRootResource, name=self.version) |
1924 | except ComponentLookupError: |
1925 | # Next, try a version-independent service root. |
1926 | service_root = getUtility(IServiceRootResource) |
1927 | |
1928 | === modified file 'src/lazr/restful/simple.py' |
1929 | --- src/lazr/restful/simple.py 2009-11-12 17:03:06 +0000 |
1930 | +++ src/lazr/restful/simple.py 2010-01-12 15:21:22 +0000 |
1931 | @@ -22,7 +22,9 @@ |
1932 | from zope.publisher.browser import BrowserRequest |
1933 | from zope.publisher.interfaces import IPublication, IPublishTraverse, NotFound |
1934 | from zope.publisher.publish import mapply |
1935 | +from zope.proxy import sameProxiedObjects |
1936 | from zope.security.management import endInteraction, newInteraction |
1937 | +from zope.traversing.browser import AbsoluteURL as ZopeAbsoluteURL |
1938 | from zope.traversing.browser.interfaces import IAbsoluteURL |
1939 | from zope.traversing.browser.absoluteurl import _insufficientContext, _safe |
1940 | |
1941 | @@ -188,7 +190,8 @@ |
1942 | # First collect the top-level collections. |
1943 | for name, (schema_interface, obj) in ( |
1944 | self.top_level_collections.items()): |
1945 | - adapter = EntryAdapterUtility.forSchemaInterface(schema_interface) |
1946 | + adapter = EntryAdapterUtility.forSchemaInterface( |
1947 | + schema_interface, self.request) |
1948 | link_name = ("%s_collection_link" % adapter.plural_type) |
1949 | top_level_resources[link_name] = obj |
1950 | # Then collect the top-level entries. |
1951 | |
1952 | === modified file 'src/lazr/restful/tales.py' |
1953 | --- src/lazr/restful/tales.py 2009-05-04 19:11:00 +0000 |
1954 | +++ src/lazr/restful/tales.py 2010-01-12 15:21:22 +0000 |
1955 | @@ -13,7 +13,8 @@ |
1956 | from epydoc.markup import DocstringLinker |
1957 | from epydoc.markup.restructuredtext import parse_docstring |
1958 | |
1959 | -from zope.component import adapts, queryAdapter, getGlobalSiteManager |
1960 | +from zope.component import ( |
1961 | + adapts, getGlobalSiteManager, getUtility, queryMultiAdapter) |
1962 | from zope.interface import implements |
1963 | from zope.interface.interfaces import IInterface |
1964 | from zope.schema import getFieldsInOrder |
1965 | @@ -31,7 +32,8 @@ |
1966 | ICollection, ICollectionField, IEntry, IJSONRequestCache, |
1967 | IReferenceChoice, IResourceDELETEOperation, IResourceGETOperation, |
1968 | IResourceOperation, IResourcePOSTOperation, IScopedCollection, |
1969 | - ITopLevelEntryLink, IWebServiceClientRequest, LAZR_WEBSERVICE_NAME) |
1970 | + ITopLevelEntryLink, IWebServiceClientRequest, IWebServiceVersion, |
1971 | + LAZR_WEBSERVICE_NAME) |
1972 | from lazr.restful.utils import get_current_browser_request |
1973 | |
1974 | |
1975 | @@ -108,13 +110,14 @@ |
1976 | @property |
1977 | def is_entry(self): |
1978 | """Whether the object is published as an entry.""" |
1979 | - return queryAdapter(self.context, IEntry) != None |
1980 | + return queryMultiAdapter( |
1981 | + (self.context, get_current_browser_request()), IEntry) != None |
1982 | |
1983 | @property |
1984 | def json(self): |
1985 | """Return a JSON description of the object.""" |
1986 | request = IWebServiceClientRequest(get_current_browser_request()) |
1987 | - if queryAdapter(self.context, IEntry): |
1988 | + if queryMultiAdapter((self.context, request), IEntry): |
1989 | resource = EntryResource(self.context, request) |
1990 | else: |
1991 | # Just dump it as JSON. |
1992 | @@ -283,9 +286,11 @@ |
1993 | # adapting. |
1994 | model_class = self._model_class |
1995 | operations = [] |
1996 | + request_interface = getUtility( |
1997 | + IWebServiceVersion, get_current_browser_request().version) |
1998 | for interface in (IResourceGETOperation, IResourcePOSTOperation): |
1999 | operations.extend(getGlobalSiteManager().adapters.lookupAll( |
2000 | - (model_class, IWebServiceClientRequest), interface)) |
2001 | + (model_class, request_interface), interface)) |
2002 | return [{'name' : name, 'op' : op} for name, op in operations] |
2003 | |
2004 | |
2005 | @@ -297,7 +302,8 @@ |
2006 | def __init__(self, entry_interface): |
2007 | super(WadlEntryInterfaceAdapterAPI, self).__init__( |
2008 | entry_interface, IEntry) |
2009 | - self.utility = EntryAdapterUtility.forEntryInterface(entry_interface) |
2010 | + self.utility = EntryAdapterUtility.forEntryInterface( |
2011 | + entry_interface, get_current_browser_request()) |
2012 | |
2013 | @property |
2014 | def entry_page_representation_link(self): |
2015 | @@ -370,8 +376,10 @@ |
2016 | @property |
2017 | def supports_delete(self): |
2018 | """Return true if this entry responds to DELETE.""" |
2019 | + request_interface = getUtility( |
2020 | + IWebServiceVersion, get_current_browser_request().version) |
2021 | operations = getGlobalSiteManager().adapters.lookupAll( |
2022 | - (self._model_class, IWebServiceClientRequest), |
2023 | + (self._model_class, request_interface), |
2024 | IResourceDELETEOperation) |
2025 | return len(operations) > 0 |
2026 | |
2027 | @@ -506,7 +514,8 @@ |
2028 | raise TypeError("Field is not of a supported type.") |
2029 | assert schema is not IObject, ( |
2030 | "Null schema provided for %s" % self.field.__name__) |
2031 | - return EntryAdapterUtility.forSchemaInterface(schema) |
2032 | + return EntryAdapterUtility.forSchemaInterface( |
2033 | + schema, get_current_browser_request()) |
2034 | |
2035 | |
2036 | @property |
2037 | @@ -530,7 +539,8 @@ |
2038 | |
2039 | def type_link(self): |
2040 | return EntryAdapterUtility.forSchemaInterface( |
2041 | - self.entry_link.entry_type).type_link |
2042 | + self.entry_link.entry_type, |
2043 | + get_current_browser_request()).type_link |
2044 | |
2045 | |
2046 | class WadlOperationAPI(RESTUtilityBase): |
2047 | |
2048 | === modified file 'src/lazr/restful/testing/webservice.py' |
2049 | --- src/lazr/restful/testing/webservice.py 2009-11-19 16:28:38 +0000 |
2050 | +++ src/lazr/restful/testing/webservice.py 2010-01-12 15:21:22 +0000 |
2051 | @@ -104,7 +104,11 @@ |
2052 | # get_current_browser_request() |
2053 | implements(IHTTPApplicationRequest, IWebServiceLayer) |
2054 | |
2055 | - def __init__(self, traversed=None, stack=None): |
2056 | + def __init__(self, traversed=None, stack=None, version=None): |
2057 | + if version is None: |
2058 | + config = getUtility(IWebServiceConfiguration) |
2059 | + version = config.latest_version_uri_prefix |
2060 | + self.version = version |
2061 | self._traversed_names = traversed |
2062 | self._stack = stack |
2063 | self.response = FakeResponse() |
2064 | |
2065 | === modified file 'src/lazr/restful/tests/test_navigation.py' |
2066 | --- src/lazr/restful/tests/test_navigation.py 2009-07-27 02:27:38 +0000 |
2067 | +++ src/lazr/restful/tests/test_navigation.py 2010-01-12 15:21:22 +0000 |
2068 | @@ -6,80 +6,101 @@ |
2069 | |
2070 | import unittest |
2071 | |
2072 | +from zope.component import getSiteManager |
2073 | from zope.interface import Interface, implements |
2074 | from zope.publisher.interfaces import NotFound |
2075 | from zope.schema import Text, Object |
2076 | +from zope.testing.cleanup import cleanUp |
2077 | |
2078 | -from lazr.restful.interfaces import IEntry |
2079 | -from lazr.restful.publisher import WebServicePublicationMixin |
2080 | +from lazr.restful.interfaces import IEntry, IWebServiceClientRequest |
2081 | +from lazr.restful.simple import Publication |
2082 | +from lazr.restful.testing.webservice import FakeRequest |
2083 | |
2084 | |
2085 | class IChild(Interface): |
2086 | + """Interface for a simple entry.""" |
2087 | one = Text(title=u'One') |
2088 | two = Text(title=u'Two') |
2089 | |
2090 | |
2091 | -class IChildEntry(IChild, IEntry): |
2092 | - pass |
2093 | - |
2094 | - |
2095 | class IParent(Interface): |
2096 | + """Interface for a simple entry that contains another entry.""" |
2097 | three = Text(title=u'Three') |
2098 | child = Object(schema=IChild) |
2099 | |
2100 | |
2101 | -class IParentEntry(IParent, IEntry): |
2102 | - pass |
2103 | - |
2104 | - |
2105 | class Child: |
2106 | - implements(IChildEntry) |
2107 | - schema = IChild |
2108 | - |
2109 | + """A simple implementation of IChild.""" |
2110 | + implements(IChild) |
2111 | one = u'one' |
2112 | two = u'two' |
2113 | |
2114 | |
2115 | +class ChildEntry: |
2116 | + """Implementation of an entry wrapping a Child.""" |
2117 | + schema = IChild |
2118 | + def __init__(self, context, request): |
2119 | + self.context = context |
2120 | + |
2121 | + |
2122 | class Parent: |
2123 | - implements(IParentEntry) |
2124 | - schema = IParent |
2125 | - |
2126 | + """A simple implementation of IParent.""" |
2127 | + implements(IParent) |
2128 | three = u'three' |
2129 | child = Child() |
2130 | |
2131 | + |
2132 | +class ParentEntry: |
2133 | + """Implementation of an entry wrapping a Parent, containing a Child.""" |
2134 | + schema = IParent |
2135 | + |
2136 | + def __init__(self, context, request): |
2137 | + self.context = context |
2138 | + |
2139 | @property |
2140 | - def context(self): |
2141 | - return self |
2142 | - |
2143 | - |
2144 | -class FakeRequest: |
2145 | + def child(self): |
2146 | + return self.context.child |
2147 | + |
2148 | + |
2149 | +class FakeRequestWithEmptyTraversalStack(FakeRequest): |
2150 | """A fake request satisfying `traverseName()`.""" |
2151 | |
2152 | def getTraversalStack(self): |
2153 | return () |
2154 | |
2155 | |
2156 | -class NavigationPublication(WebServicePublicationMixin): |
2157 | - pass |
2158 | - |
2159 | - |
2160 | class NavigationTestCase(unittest.TestCase): |
2161 | |
2162 | + def setUp(self): |
2163 | + # Register ChildEntry as the IEntry implementation for IChild. |
2164 | + sm = getSiteManager() |
2165 | + sm.registerAdapter( |
2166 | + ChildEntry, [IChild, IWebServiceClientRequest], provided=IEntry) |
2167 | + |
2168 | + # Register ParentEntry as the IEntry implementation for IParent. |
2169 | + sm.registerAdapter( |
2170 | + ParentEntry, [IParent, IWebServiceClientRequest], provided=IEntry) |
2171 | + |
2172 | + def tearDown(self): |
2173 | + cleanUp() |
2174 | + |
2175 | def test_toplevel_navigation(self): |
2176 | # Test that publication can reach sub-entries. |
2177 | - publication = NavigationPublication() |
2178 | - obj = publication.traverseName(FakeRequest(), Parent(), 'child') |
2179 | + publication = Publication(None) |
2180 | + request = FakeRequestWithEmptyTraversalStack(version='trunk') |
2181 | + obj = publication.traverseName(request, Parent(), 'child') |
2182 | self.assertEqual(obj.one, 'one') |
2183 | |
2184 | def test_toplevel_navigation_without_subentry(self): |
2185 | # Test that publication raises NotFound when subentry attribute |
2186 | # returns None. |
2187 | + request = FakeRequestWithEmptyTraversalStack(version='trunk') |
2188 | parent = Parent() |
2189 | parent.child = None |
2190 | - publication = NavigationPublication() |
2191 | + publication = Publication(None) |
2192 | self.assertRaises( |
2193 | NotFound, publication.traverseName, |
2194 | - FakeRequest(), parent, 'child') |
2195 | + request, parent, 'child') |
2196 | |
2197 | |
2198 | def additional_tests(): |
2199 | |
2200 | === modified file 'src/lazr/restful/tests/test_webservice.py' |
2201 | --- src/lazr/restful/tests/test_webservice.py 2009-08-11 18:36:20 +0000 |
2202 | +++ src/lazr/restful/tests/test_webservice.py 2010-01-12 15:21:22 +0000 |
2203 | @@ -9,22 +9,26 @@ |
2204 | from types import ModuleType |
2205 | import unittest |
2206 | |
2207 | -from zope.component import getGlobalSiteManager |
2208 | +from zope.component import getGlobalSiteManager, getUtility |
2209 | from zope.configuration import xmlconfig |
2210 | -from zope.interface import implements, Interface |
2211 | +from zope.interface import alsoProvides, implements, Interface |
2212 | from zope.schema import Date, Datetime, TextLine |
2213 | from zope.testing.cleanup import CleanUp |
2214 | |
2215 | from lazr.restful.fields import Reference |
2216 | from lazr.restful.interfaces import ( |
2217 | ICollection, IEntry, IEntryResource, IResourceGETOperation, |
2218 | - IWebServiceClientRequest) |
2219 | + IWebServiceClientRequest, IWebServiceVersion) |
2220 | from lazr.restful import EntryResource, ServiceRootResource, ResourceGETOperation |
2221 | -from lazr.restful.simple import Request |
2222 | +from lazr.restful.simple import BaseWebServiceConfiguration, Request |
2223 | from lazr.restful.declarations import ( |
2224 | collection_default_content, exported, export_as_webservice_collection, |
2225 | export_as_webservice_entry, export_read_operation, operation_parameters) |
2226 | +from lazr.restful.interfaces import IWebServiceConfiguration |
2227 | +from lazr.restful.testing.webservice import ( |
2228 | + create_web_service_request, WebServiceTestPublication) |
2229 | from lazr.restful.testing.tales import test_tales |
2230 | +from lazr.restful.utils import get_current_browser_request |
2231 | |
2232 | |
2233 | def get_resource_factory(model_interface, resource_interface): |
2234 | @@ -36,8 +40,9 @@ |
2235 | `IEntry` or `ICollection`. |
2236 | :return: the resource factory (the autogenerated adapter class. |
2237 | """ |
2238 | - return getGlobalSiteManager().adapters.lookup1( |
2239 | - model_interface, resource_interface) |
2240 | + request_interface = getUtility(IWebServiceVersion, name='trunk') |
2241 | + return getGlobalSiteManager().adapters.lookup( |
2242 | + (model_interface, request_interface), resource_interface) |
2243 | |
2244 | |
2245 | def get_operation_factory(model_interface, name): |
2246 | @@ -87,6 +92,23 @@ |
2247 | """Returns all the entries.""" |
2248 | |
2249 | |
2250 | +class SimpleWebServiceConfiguration(BaseWebServiceConfiguration): |
2251 | + implements(IWebServiceConfiguration) |
2252 | + show_tracebacks = False |
2253 | + latest_version_uri_prefix = 'trunk' |
2254 | + hostname = "webservice_test" |
2255 | + |
2256 | + def createRequest(self, body_instream, environ): |
2257 | + request = Request(body_instream, environ) |
2258 | + request.setPublication(WebServiceTestPublication(None)) |
2259 | + request.version = 'trunk' |
2260 | + return request |
2261 | + |
2262 | + |
2263 | +class IWebServiceRequestTrunk(IWebServiceClientRequest): |
2264 | + """A marker interface for requests to the 'trunk' web service.""" |
2265 | + |
2266 | + |
2267 | class WebServiceTestCase(CleanUp, unittest.TestCase): |
2268 | """A test case for web service operations.""" |
2269 | |
2270 | @@ -96,6 +118,17 @@ |
2271 | """Set the component registry with the given model.""" |
2272 | super(WebServiceTestCase, self).setUp() |
2273 | |
2274 | + # Register a simple configuration object. |
2275 | + webservice_configuration = SimpleWebServiceConfiguration() |
2276 | + sm = getGlobalSiteManager() |
2277 | + sm.registerUtility(webservice_configuration) |
2278 | + |
2279 | + # Register an IWebServiceVersion for the |
2280 | + # 'trunk' web service version. |
2281 | + alsoProvides(IWebServiceRequestTrunk, IWebServiceVersion) |
2282 | + sm.registerUtility( |
2283 | + IWebServiceRequestTrunk, IWebServiceVersion, name='trunk') |
2284 | + |
2285 | # Build a test module that exposes the given resource interfaces. |
2286 | testmodule = ModuleType('testmodule') |
2287 | for interface in self.testmodule_objects: |
2288 | @@ -195,8 +228,8 @@ |
2289 | entry_class = get_resource_factory(IHasRestrictedField, IEntry) |
2290 | request = Request(StringIO(""), {}) |
2291 | |
2292 | - entry = entry_class(HasRestrictedField("")) |
2293 | - resource = EntryResource(entry, request) |
2294 | + entry = entry_class(HasRestrictedField(""), request) |
2295 | + resource = EntryResource(HasRestrictedField(""), request) |
2296 | |
2297 | entry.schema['a_field'].restrict_to_interface = IHasRestrictedField |
2298 | resource.applyChanges({'a_field': 'a_value'}) |
2299 | @@ -308,6 +341,7 @@ |
2300 | This will fail due to a name conflict. |
2301 | """ |
2302 | resource = ServiceRootResource() |
2303 | + request = create_web_service_request('/') |
2304 | try: |
2305 | resource.toWADL() |
2306 | self.fail('Expected toWADL to fail with an AssertionError') |
2307 | |
2308 | === modified file 'src/lazr/restful/utils.py' |
2309 | --- src/lazr/restful/utils.py 2009-10-12 20:47:21 +0000 |
2310 | +++ src/lazr/restful/utils.py 2010-01-12 15:21:22 +0000 |
2311 | @@ -7,6 +7,7 @@ |
2312 | 'camelcase_to_underscore_separated', |
2313 | 'get_current_browser_request', |
2314 | 'implement_from_dict', |
2315 | + 'make_identifier_safe', |
2316 | 'safe_js_escape', |
2317 | 'safe_hasattr', |
2318 | 'smartquote', |
2319 | @@ -16,6 +17,7 @@ |
2320 | |
2321 | import cgi |
2322 | import re |
2323 | +import string |
2324 | import subprocess |
2325 | |
2326 | from simplejson import encoder |
2327 | @@ -51,6 +53,21 @@ |
2328 | return new_class |
2329 | |
2330 | |
2331 | +def make_identifier_safe(name): |
2332 | + """Change a string so it can be used as a Python identifier. |
2333 | + |
2334 | + Changes all characters other than letters, numbers, and underscore |
2335 | + into underscore. If the first character is not a letter or |
2336 | + underscore, prepends an underscore. |
2337 | + """ |
2338 | + if name is None: |
2339 | + raise ValueError("Cannot make None value identifier-safe.") |
2340 | + name = re.sub("[^A-Za-z0-9_]", "_", name) |
2341 | + if len(name) == 0 or name[0] not in string.letters and name[0] != '_': |
2342 | + name = '_' + name |
2343 | + return name |
2344 | + |
2345 | + |
2346 | def camelcase_to_underscore_separated(name): |
2347 | """Convert 'ACamelCaseString' to 'a_camel_case_string'""" |
2348 | def prepend_underscore(match): |
This branch makes it possible to serve multiple versions of a web service from the same underlying dataset. The multiversion.txt test shows how it's done. Everything else in this branch is code to make existing lazr.restful code version-aware without breaking the existing tests.
As per the 'version- specific- request- interface' branch, every incoming request is associated with some version of the web service using a marker interface. To make lazr.restful code version-aware I made it request-aware. Instead of ILocation I now use IRequestAwareLo cation. IEntry and ICollection lookups are now multi-adapter lookups: previously you could just write "IEntry( data_model_ object) ". Now you must write "getMultiAdapte r((data_ model_object, request), IEntry)" -- depending on the marker interface attached to the request object, you may get a different IEntry implementation back.
I had to add some more setup to the unit tests. Previously they got along without defining any configuration or request objects. Now that IEntry lookups require a request object, I had to define fake requests and minimal configuration objects for the unit tests.
There are a few TK places I need to look into. I also use code like this several times:
+ request_interface = getUtility( tRequestImpleme ntation, request. version) nager() .adapters. lookup(
+ IVersionedClien
+ name=self.
+ return getGlobalSiteMa
+ (model_schema, request_interface), IEntry).schema
It might be worth writing a helper function.
I'm going through the code now and I'll comment on anything else strange I find.