Merge lp://staging/~leonardr/lazr.restful/multiversion-collection into lp://staging/lazr.restful

Proposed by Leonard Richardson
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
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) code Approve
Review via email: mp+16924@code.staging.launchpad.net
To post a comment you must log in.
Revision history for this message
Leonard Richardson (leonardr) wrote :

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 IRequestAwareLocation. IEntry and ICollection lookups are now multi-adapter lookups: previously you could just write "IEntry(data_model_object)". Now you must write "getMultiAdapter((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(
+ IVersionedClientRequestImplementation,
+ name=self.request.version)
+ return getGlobalSiteManager().adapters.lookup(
+ (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.

Revision history for this message
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.

Revision history for this message
Francis J. Lacoste (flacoste) wrote :
Download full text (12.9 KiB)

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/restful/_resource.py'
> class EntryFieldURL(AbsoluteURL):
> """An IAbsoluteURL adapter for EntryField objects."""
> component.adapts(EntryField, IHTTPRequest)
> @@ -1243,7 +1246,7 @@
> def __init__(self, context, request):
> """Associate this resource with a specific object and request."""
> super(EntryResource, self).__init__(context, request)
> - self.entry = IEntry(context)
> + self.entry = getMultiAdapter((context, request), IEntry)
>

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.relationship.value_type.schema
> - return getGlobalSiteManager().adapters.lookup1(
> - model_schema, IEntry).schema
> + request_interface = getUtility(
> + IVersionedClientRequestImplementation,
> + name=self.request.version)
> + return getGlobalSiteManager().adapters.lookup(
> + (model_schema, request_interface), IEntry).schema

In my previous review, I suggested I...Factory instead of
IVersionedClientRequestImplementation, here I see that it's not a 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.

> === modified file 'src/lazr/restful/directives/__init__.py'

> + utility = cls()
> + sm = getSiteManager()
> + sm.registerUtility(utility, IWebServiceConfiguration)
> +
> + # Create and register marker interfaces for request objects.
> + for version in set(
> + utility.active_versions + [utility.latest_version_uri_prefix]):
> + classname = ("IWebServiceClientRequestVersion" +
> + version.replace('.', '_'))

There are probably other characters that need to be squashed '-' is likely to
be used.

> + marker_interface = InterfaceClass(
> + classname, (IWebServiceClientRequest,), {})
> + alsoProvides(
> + marker_interface, IVersionedClientRequestImplementation)
> + sm.registerUtility(
> + marker_interface, IVersionedClientRequestImplementation,
> + name=version)
> return True

I suggest writing a unit test for that part that would cover the mangling.

> === modified file 'src/lazr/restful/docs/multiversion.txt'
> +URL generation
> +--------------
> +
> + >>> from zope.component import getSiteManager
> + >>> from zope.traversing.browser...

review: Needs Fixing (code)
Revision history for this message
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.relationship.value_type.schema
> > - return getGlobalSiteManager().adapters.lookup1(
> > - model_schema, IEntry).schema
> > + request_interface = getUtility(
> > + IVersionedClientRequestImplementation,
> > + name=self.request.version)
> > + return getGlobalSiteManager().adapters.lookup(
> > + (model_schema, request_interface), IEntry).schema
>
> In my previous review, I suggested I...Factory instead of
> IVersionedClientRequestImplementation, here I see that it's not a 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?

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 WebServiceClientRequest.marker_interface. Is that what you meant? (I could write this property method now, with the utility lookup, and it would clean up the code a bit.)

Revision history for this message
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.relationship.value_type.schema
> > > - return getGlobalSiteManager().adapters.lookup1(
> > > - model_schema, IEntry).schema
> > > + request_interface = getUtility(
> > > + IVersionedClientRequestImplementation,
> > > + name=self.request.version)
> > > + return getGlobalSiteManager().adapters.lookup(
> > > + (model_schema, request_interface), IEntry).schema
> >
> > In my previous review, I suggested I...Factory instead of
> > IVersionedClientRequestImplementation, here I see that it's not a
> > 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 WebServiceClientRequest.marker_interface. Is that what you
> 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>

Revision history for this message
Leonard Richardson (leonardr) wrote :
Download full text (13.0 KiB)

> > === modified file 'src/lazr/restful/_resource.py'
> > class EntryFieldURL(AbsoluteURL):
> > """An IAbsoluteURL adapter for EntryField objects."""
> > component.adapts(EntryField, IHTTPRequest)
> > @@ -1243,7 +1246,7 @@
> > def __init__(self, context, request):
> > """Associate this resource with a specific object and request."""
> > super(EntryResource, self).__init__(context, request)
> > - self.entry = IEntry(context)
> > + self.entry = getMultiAdapter((context, request), IEntry)
> >
>
> 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.active_versions + [utility.latest_version_uri_prefix]):
> > + classname = ("IWebServiceClientRequestVersion" +
> > + version.replace('.', '_'))
>
> There are probably other characters that need to be squashed '-' is likely to
> be used.

I was lazy. I've added a make_identifier_safe function to lazr.restful.utils and a doctest for it.

> > === modified file 'src/lazr/restful/docs/multiversion.txt'
> > +URL generation
> > +--------------
> > +
> > + >>> from zope.component import getSiteManager
> > + >>> from zope.traversing.browser.interfaces import IAbsoluteURL
> > + >>> from lazr.restful.interfaces import (
> > + ... IRequestAwareLocation, IWebServiceLayer)
> > + >>> from lazr.restful.simple import AbsoluteURL
> > + >>> sm = getSiteManager()
> > + >>> sm.registerAdapter(
> > + ... AbsoluteURL, (IRequestAwareLocation, IWebServiceLayer),
> > + ... provided=IAbsoluteURL)
>
> 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.interfaces import ITraverseWithGet
> > >>> class IContactSet(ITestDataObject, ITraverseWithGet):
> > - ... 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. ITraverseWithGet.get() needs to take a request as an argument because the traversal rules can change from one version to another. But they don't change in this case. I've removed the redundant definition.

> > Here's a simple ContactSet with a predefined list of contacts.
> >
> > + >>> from zope.publisher.interfaces.browser...

Revision history for this message
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.

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

Apropos getting rid of IRequestAwareLocation: I think you were envisioning some code like this:

    >>> class ContactSetLocation(object):
    ... """An ILocation implementation for ContactSet objects."""
    ... implements(ILocation)
    ...
    ... def __init__(self, context, request):
    ... self.context = contact_set
    ... self.request = request
    ...
    ... def __parent__(self, request):
    ... return getUtility(IServiceRootResource, name=request.version)
    ...
    ... def __name__(self, request):
    ... if request.version == 'beta':
    ... return 'contact_list'
    ... return 'contacts'

    >>> from zope.traversing.browser import AbsoluteURL as ZopeAbsoluteURL
    >>> sm.registerAdapter(
    ... ZopeAbsoluteURL, (ILocation, IWebServiceLayer),
    ... provided=IAbsoluteURL)

    >>> 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 IRequestAwareLocation interface. Does this make sense? Am I missing something?

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

On January 11, 2010, Leonard Richardson wrote:
> > > === modified file 'src/lazr/restful/_resource.py'
> > > class EntryFieldURL(AbsoluteURL):
> > > """An IAbsoluteURL adapter for EntryField objects."""
> > > component.adapts(EntryField, IHTTPRequest)
> > > @@ -1243,7 +1246,7 @@
> > > def __init__(self, context, request):
> > > """Associate this resource with a specific object and
> > > request.""" super(EntryResource, self).__init__(context, request) -
> > > self.entry = IEntry(context)
> > > + self.entry = getMultiAdapter((context, request), IEntry)
> >
> > 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.interfaces.browser import IBrowserRequest
> > > + >>> from lazr.restful.interfaces import IServiceRootResource
> > >
> > > >>> from lazr.restful.simple import TraverseWithGet
> > >
> > > - >>> from zope.publisher.interfaces.browser import IBrowserRequest
> > >
> > > >>> class ContactSet(TraverseWithGet):
> > >
> > > ... implements(IContactSet)
> > > - ... 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>

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

On January 11, 2010, Leonard Richardson wrote:
> Apropos getting rid of IRequestAwareLocation: I think you were envisioning
some code like this:
> >>> class ContactSetLocation(object):
>
> ... """An ILocation implementation for ContactSet objects."""
> ... implements(ILocation)
> ...
> ... def __init__(self, context, request):
> ... self.context = contact_set
> ... self.request = request
> ...
> ... def __parent__(self, request):
> ... return getUtility(IServiceRootResource,
> name=request.version) ...
> ... 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.browser import AbsoluteURL as ZopeAbsoluteURL
> >>> sm.registerAdapter(
>
> ... ZopeAbsoluteURL, (ILocation, IWebServiceLayer),
> ... provided=IAbsoluteURL)
>
> >>> 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_browser_request() to get the request parameter.

>
> 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
> IRequestAwareLocation interface. Does this make sense? Am I missing
> something?
>

--
Francis J. Lacoste
<email address hidden>

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

If I use get_current_browser_request() I shouldn't need an ILocation 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 IRequestAwareLocation thing. We can always implement a standard ILocation adapter that grabs the request and branches based on request.version. But with IRequestAwareLocation we can define totally different adapters for 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.

Revision history for this message
Francis J. Lacoste (flacoste) wrote :

On January 12, 2010, Leonard Richardson wrote:
> If I use get_current_browser_request() I shouldn't need an ILocation
> 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 IRequestAwareLocation
> thing. We can always implement a standard ILocation adapter that grabs the
> request and branches based on request.version. But with
> IRequestAwareLocation we can define totally different adapters for
> 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>

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

OK, I've made all the changes you requested. The complete diff of my changes since your review is here:

http://pastebin.ubuntu.com/355558/

Revision history for this message
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://pastebin.ubuntu.com/355558/
>

That's great, thanks. My only comment is about the NEWS.txt file:

> === modified file 'src/lazr/restful/NEWS.txt'
> --- src/lazr/restful/NEWS.txt 2009-11-16 14:49:53 +0000
> +++ src/lazr/restful/NEWS.txt 2010-01-12 15:17:51 +0000
> @@ -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>

review: Approve (code)
95. By Leonard Richardson

[r=flacoste] It's now possible to define two distinct web services based on the same data model.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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):

Subscribers

People subscribed via source and target branches