Merge lp:~leonardr/lazr.restful/multiversion-collection into lp: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:~leonardr/lazr.restful/multiversion-collection
Merge into: lp:lazr.restful
Prerequisite: lp:~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:~leonardr/lazr.restful/multiversion-collection
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) code Approve
Review via email: mp+16924@code.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
=== 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:21:22 +0000
@@ -9,10 +9,11 @@
9changes. You *must* change your configuration object to get your code9changes. You *must* change your configuration object to get your code
10to work in this version! See "active_versions" below.10to work in this version! See "active_versions" below.
1111
12Added the precursor of a versioning system for web services. Clients12Added a versioning system for web services. Clients can now request
13can now request the "trunk" of a web service as well as one published13the "trunk" of a web service as well as one published version. Apart
14version. Apart from the URIs served, the two web services are exactly14from the URIs served, the two web services are exactly the same. There
15the same.15is no way to serve two different versions of a web service without
16defining both versions from scratch.
1617
17This release introduces a new field to IWebServiceConfiguration:18This release introduces a new field to IWebServiceConfiguration:
18latest_version_uri_prefix. If you are rolling your own19latest_version_uri_prefix. If you are rolling your own
1920
=== modified file 'src/lazr/restful/_operation.py'
--- src/lazr/restful/_operation.py 2009-03-31 17:58:53 +0000
+++ src/lazr/restful/_operation.py 2010-01-12 15:21:22 +0000
@@ -4,7 +4,7 @@
44
5import simplejson5import simplejson
66
7from zope.component import getMultiAdapter, queryAdapter7from zope.component import getMultiAdapter, queryMultiAdapter
8from zope.event import notify8from zope.event import notify
9from zope.interface import Attribute, implements, providedBy9from zope.interface import Attribute, implements, providedBy
10from zope.interface.interfaces import IInterface10from zope.interface.interfaces import IInterface
@@ -80,11 +80,11 @@
80 # this object served to the client.80 # this object served to the client.
81 return result81 return result
8282
83 if queryAdapter(result, ICollection):83 if queryMultiAdapter((result, self.request), ICollection):
84 # If the result is a web service collection, serve only one84 # If the result is a web service collection, serve only one
85 # batch of the collection.85 # batch of the collection.
86 result = CollectionResource(86 collection = getMultiAdapter((result, self.request), ICollection)
87 ICollection(result), self.request).batch()87 result = CollectionResource(collection, self.request).batch()
88 elif self.should_batch(result):88 elif self.should_batch(result):
89 result = self.batch(result, self.request)89 result = self.batch(result, self.request)
9090
@@ -93,7 +93,7 @@
93 try:93 try:
94 json_representation = simplejson.dumps(94 json_representation = simplejson.dumps(
95 result, cls=ResourceJSONEncoder)95 result, cls=ResourceJSONEncoder)
96 except TypeError:96 except TypeError, e:
97 raise TypeError("Could not serialize object %s to JSON." %97 raise TypeError("Could not serialize object %s to JSON." %
98 result)98 result)
9999
100100
=== modified file 'src/lazr/restful/_resource.py'
--- src/lazr/restful/_resource.py 2009-10-27 17:45:53 +0000
+++ src/lazr/restful/_resource.py 2010-01-12 15:21:22 +0000
@@ -18,6 +18,7 @@
18 'JSONItem',18 'JSONItem',
19 'ReadOnlyResource',19 'ReadOnlyResource',
20 'RedirectResource',20 'RedirectResource',
21 'register_versioned_request_utility',
21 'render_field_to_html',22 'render_field_to_html',
22 'ResourceJSONEncoder',23 'ResourceJSONEncoder',
23 'RESTUtilityBase',24 'RESTUtilityBase',
@@ -47,13 +48,15 @@
47from zope.app.pagetemplate.engine import TrustedAppPT48from zope.app.pagetemplate.engine import TrustedAppPT
48from zope import component49from zope import component
49from zope.component import (50from zope.component import (
50 adapts, getAdapters, getAllUtilitiesRegisteredFor, getMultiAdapter,51 adapts, getAdapters, getAllUtilitiesRegisteredFor,
51 getUtility, queryAdapter, getGlobalSiteManager)52 getGlobalSiteManager, getMultiAdapter, getSiteManager, getUtility,
53 queryMultiAdapter)
52from zope.component.interfaces import ComponentLookupError54from zope.component.interfaces import ComponentLookupError
53from zope.event import notify55from zope.event import notify
54from zope.publisher.http import init_status_codes, status_reasons56from zope.publisher.http import init_status_codes, status_reasons
55from zope.interface import (57from zope.interface import (
56 implementer, implements, implementedBy, providedBy, Interface)58 alsoProvides, implementer, implements, implementedBy, providedBy,
59 Interface)
57from zope.interface.common.sequence import IFiniteSequence60from zope.interface.common.sequence import IFiniteSequence
58from zope.interface.interfaces import IInterface61from zope.interface.interfaces import IInterface
59from zope.location.interfaces import ILocation62from zope.location.interfaces import ILocation
@@ -82,7 +85,8 @@
82 IResourceDELETEOperation, IResourceGETOperation, IResourcePOSTOperation,85 IResourceDELETEOperation, IResourceGETOperation, IResourcePOSTOperation,
83 IScopedCollection, IServiceRootResource, ITopLevelEntryLink,86 IScopedCollection, IServiceRootResource, ITopLevelEntryLink,
84 IUnmarshallingDoesntNeedValue, IWebServiceClientRequest,87 IUnmarshallingDoesntNeedValue, IWebServiceClientRequest,
85 IWebServiceConfiguration, LAZR_WEBSERVICE_NAME)88 IWebServiceConfiguration, IWebServiceLayer, IWebServiceVersion,
89 LAZR_WEBSERVICE_NAME)
86from lazr.restful.utils import get_current_browser_request90from lazr.restful.utils import get_current_browser_request
8791
8892
@@ -112,6 +116,17 @@
112 return unicode(value)116 return unicode(value)
113117
114118
119def register_versioned_request_utility(interface, version):
120 """Registers a marker interface as a utility for version lookup.
121
122 This function registers the given interface class as the
123 IWebServiceVersion utility for the given version string.
124 """
125 alsoProvides(interface, IWebServiceVersion)
126 getSiteManager().registerUtility(
127 interface, IWebServiceVersion, name=version)
128
129
115class LazrPageTemplateFile(TrustedAppPT, PageTemplateFile):130class LazrPageTemplateFile(TrustedAppPT, PageTemplateFile):
116 "A page template class for generating web service-related documents."131 "A page template class for generating web service-related documents."
117 pass132 pass
@@ -143,8 +158,9 @@
143 return tuple(obj)158 return tuple(obj)
144 if isinstance(underlying_object, dict):159 if isinstance(underlying_object, dict):
145 return dict(obj)160 return dict(obj)
146 if queryAdapter(obj, IEntry):161 request = get_current_browser_request()
147 obj = EntryResource(obj, get_current_browser_request())162 if queryMultiAdapter((obj, request), IEntry):
163 obj = EntryResource(obj, request)
148164
149 return IJSONPublishable(obj).toDataForJSON()165 return IJSONPublishable(obj).toDataForJSON()
150166
@@ -1220,6 +1236,7 @@
1220 self.__parent__ = self.entry.context1236 self.__parent__ = self.entry.context
1221 self.__name__ = self.name1237 self.__name__ = self.name
12221238
1239
1223class EntryFieldURL(AbsoluteURL):1240class EntryFieldURL(AbsoluteURL):
1224 """An IAbsoluteURL adapter for EntryField objects."""1241 """An IAbsoluteURL adapter for EntryField objects."""
1225 component.adapts(EntryField, IHTTPRequest)1242 component.adapts(EntryField, IHTTPRequest)
@@ -1243,7 +1260,7 @@
1243 def __init__(self, context, request):1260 def __init__(self, context, request):
1244 """Associate this resource with a specific object and request."""1261 """Associate this resource with a specific object and request."""
1245 super(EntryResource, self).__init__(context, request)1262 super(EntryResource, self).__init__(context, request)
1246 self.entry = IEntry(context)1263 self.entry = getMultiAdapter((context, request), IEntry)
12471264
1248 def _getETagCore(self, unmarshalled_field_values=None):1265 def _getETagCore(self, unmarshalled_field_values=None):
1249 """Calculate the ETag for an entry.1266 """Calculate the ETag for an entry.
@@ -1442,7 +1459,10 @@
1442 def __init__(self, context, request):1459 def __init__(self, context, request):
1443 """Associate this resource with a specific object and request."""1460 """Associate this resource with a specific object and request."""
1444 super(CollectionResource, self).__init__(context, request)1461 super(CollectionResource, self).__init__(context, request)
1445 self.collection = ICollection(context)1462 if ICollection.providedBy(context):
1463 self.collection = context
1464 else:
1465 self.collection = getMultiAdapter((context, request), ICollection)
14461466
1447 def do_GET(self):1467 def do_GET(self):
1448 """Fetch a collection and render it as JSON."""1468 """Fetch a collection and render it as JSON."""
@@ -1492,12 +1512,14 @@
1492 # Scoped collection. The type URL depends on what type of1512 # Scoped collection. The type URL depends on what type of
1493 # entry the collection holds.1513 # entry the collection holds.
1494 schema = self.context.relationship.value_type.schema1514 schema = self.context.relationship.value_type.schema
1495 adapter = EntryAdapterUtility.forSchemaInterface(schema)1515 adapter = EntryAdapterUtility.forSchemaInterface(
1516 schema, self.request)
1496 return adapter.entry_page_type_link1517 return adapter.entry_page_type_link
1497 else:1518 else:
1498 # Top-level collection.1519 # Top-level collection.
1499 schema = self.collection.entry_schema1520 schema = self.collection.entry_schema
1500 adapter = EntryAdapterUtility.forEntryInterface(schema)1521 adapter = EntryAdapterUtility.forEntryInterface(
1522 schema, self.request)
1501 return adapter.collection_type_link1523 return adapter.collection_type_link
15021524
15031525
@@ -1588,7 +1610,7 @@
1588 # class's singular or plural names.1610 # class's singular or plural names.
1589 schema = registration.required[0]1611 schema = registration.required[0]
1590 adapter = EntryAdapterUtility.forSchemaInterface(1612 adapter = EntryAdapterUtility.forSchemaInterface(
1591 schema)1613 schema, self.request)
15921614
1593 singular = adapter.singular_type1615 singular = adapter.singular_type
1594 assert not singular_names.has_key(singular), (1616 assert not singular_names.has_key(singular), (
@@ -1662,7 +1684,7 @@
1662 # It's not a top-level resource.1684 # It's not a top-level resource.
1663 continue1685 continue
1664 adapter = EntryAdapterUtility.forEntryInterface(1686 adapter = EntryAdapterUtility.forEntryInterface(
1665 entry_schema)1687 entry_schema, self.request)
1666 link_name = ("%s_collection_link" % adapter.plural_type)1688 link_name = ("%s_collection_link" % adapter.plural_type)
1667 top_level_resources[link_name] = utility1689 top_level_resources[link_name] = utility
1668 # Now, collect the top-level entries.1690 # Now, collect the top-level entries.
@@ -1687,26 +1709,28 @@
1687 """An individual entry."""1709 """An individual entry."""
1688 implements(IEntry)1710 implements(IEntry)
16891711
1690 def __init__(self, context):1712 def __init__(self, context, request):
1691 """Associate the entry with some database model object."""1713 """Associate the entry with some database model object."""
1692 self.context = context1714 self.context = context
1715 self.request = request
16931716
16941717
1695class Collection:1718class Collection:
1696 """A collection of entries."""1719 """A collection of entries."""
1697 implements(ICollection)1720 implements(ICollection)
16981721
1699 def __init__(self, context):1722 def __init__(self, context, request):
1700 """Associate the entry with some database model object."""1723 """Associate the entry with some database model object."""
1701 self.context = context1724 self.context = context
1725 self.request = request
17021726
17031727
1704class ScopedCollection:1728class ScopedCollection:
1705 """A collection associated with some parent object."""1729 """A collection associated with some parent object."""
1706 implements(IScopedCollection)1730 implements(IScopedCollection)
1707 adapts(Interface, Interface)1731 adapts(Interface, Interface, IWebServiceLayer)
17081732
1709 def __init__(self, context, collection):1733 def __init__(self, context, collection, request):
1710 """Initialize the scoped collection.1734 """Initialize the scoped collection.
17111735
1712 :param context: The object to which the collection is scoped.1736 :param context: The object to which the collection is scoped.
@@ -1714,6 +1738,7 @@
1714 """1738 """
1715 self.context = context1739 self.context = context
1716 self.collection = collection1740 self.collection = collection
1741 self.request = request
1717 # Unknown at this time. Should be set by our call-site.1742 # Unknown at this time. Should be set by our call-site.
1718 self.relationship = None1743 self.relationship = None
17191744
@@ -1723,8 +1748,11 @@
1723 # We are given a model schema (IFoo). Look up the1748 # We are given a model schema (IFoo). Look up the
1724 # corresponding entry schema (IFooEntry).1749 # corresponding entry schema (IFooEntry).
1725 model_schema = self.relationship.value_type.schema1750 model_schema = self.relationship.value_type.schema
1726 return getGlobalSiteManager().adapters.lookup1(1751 request_interface = getUtility(
1727 model_schema, IEntry).schema1752 IWebServiceVersion,
1753 name=self.request.version)
1754 return getGlobalSiteManager().adapters.lookup(
1755 (model_schema, request_interface), IEntry).schema
17281756
1729 def find(self):1757 def find(self):
1730 """See `ICollection`."""1758 """See `ICollection`."""
@@ -1748,33 +1776,42 @@
1748 """1776 """
17491777
1750 @classmethod1778 @classmethod
1751 def forSchemaInterface(cls, entry_interface):1779 def forSchemaInterface(cls, entry_interface, request):
1752 """Create an entry adapter utility, given a schema interface.1780 """Create an entry adapter utility, given a schema interface.
17531781
1754 A schema interface is one that can be annotated to produce a1782 A schema interface is one that can be annotated to produce a
1755 subclass of IEntry.1783 subclass of IEntry.
1756 """1784 """
1785 request_interface = getUtility(
1786 IWebServiceVersion, name=request.version)
1757 entry_class = getGlobalSiteManager().adapters.lookup(1787 entry_class = getGlobalSiteManager().adapters.lookup(
1758 (entry_interface,), IEntry)1788 (entry_interface, request_interface), IEntry)
1759 assert entry_class is not None, (1789 assert entry_class is not None, (
1760 "No IEntry adapter found for %s." % entry_interface.__name__)1790 ("No IEntry adapter found for %s (web service version: %s)."
1791 % (entry_interface.__name__, request.version)))
1761 return EntryAdapterUtility(entry_class)1792 return EntryAdapterUtility(entry_class)
17621793
1763 @classmethod1794 @classmethod
1764 def forEntryInterface(cls, entry_interface):1795 def forEntryInterface(cls, entry_interface, request):
1765 """Create an entry adapter utility, given a subclass of IEntry."""1796 """Create an entry adapter utility, given a subclass of IEntry."""
1766 registrations = getGlobalSiteManager().registeredAdapters()1797 registrations = getGlobalSiteManager().registeredAdapters()
1798 # There should be one IEntry subclass registered for every
1799 # version of the web service. We'll go through the appropriate
1800 # IEntry registrations looking for one associated with the
1801 # same IWebServiceVersion interface we find on the 'request'
1802 # object.
1767 entry_classes = [1803 entry_classes = [
1768 registration.factory for registration in registrations1804 registration.factory for registration in registrations
1769 if (IInterface.providedBy(registration.provided)1805 if (IInterface.providedBy(registration.provided)
1770 and registration.provided.isOrExtends(IEntry)1806 and registration.provided.isOrExtends(IEntry)
1771 and entry_interface.implementedBy(registration.factory))]1807 and entry_interface.implementedBy(registration.factory)
1808 and registration.required[1].providedBy(request))]
1772 assert not len(entry_classes) > 1, (1809 assert not len(entry_classes) > 1, (
1773 "%s provides more than one IEntry subclass." %1810 "%s provides more than one IEntry subclass for version %s." %
1774 entry_interface.__name__)1811 entry_interface.__name__, request.version)
1775 assert not len(entry_classes) < 1, (1812 assert not len(entry_classes) < 1, (
1776 "%s does not provide any IEntry subclass." %1813 "%s does not provide any IEntry subclass for version %s." %
1777 entry_interface.__name__)1814 entry_interface.__name__, request.version)
1778 return EntryAdapterUtility(entry_classes[0])1815 return EntryAdapterUtility(entry_classes[0])
17791816
1780 def __init__(self, entry_class):1817 def __init__(self, entry_class):
17811818
=== modified file 'src/lazr/restful/declarations.py'
--- src/lazr/restful/declarations.py 2009-08-14 16:11:34 +0000
+++ src/lazr/restful/declarations.py 2010-01-12 15:21:22 +0000
@@ -51,12 +51,12 @@
51from lazr.restful.interface import copy_field51from lazr.restful.interface import copy_field
52from lazr.restful.interfaces import (52from lazr.restful.interfaces import (
53 ICollection, IEntry, IResourceDELETEOperation, IResourceGETOperation,53 ICollection, IEntry, IResourceDELETEOperation, IResourceGETOperation,
54 IResourcePOSTOperation, IWebServiceConfiguration, LAZR_WEBSERVICE_NAME,54 IResourcePOSTOperation, IWebServiceConfiguration, IWebServiceVersion,
55 LAZR_WEBSERVICE_NS)55 LAZR_WEBSERVICE_NAME, LAZR_WEBSERVICE_NS)
56from lazr.restful import (56from lazr.restful import (
57 Collection, Entry, EntryAdapterUtility, ResourceOperation, ObjectLink)57 Collection, Entry, EntryAdapterUtility, ResourceOperation, ObjectLink)
58from lazr.restful.security import protect_schema58from lazr.restful.security import protect_schema
59from lazr.restful.utils import camelcase_to_underscore_separated59from lazr.restful.utils import camelcase_to_underscore_separated, get_current_browser_request
6060
61LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS61LAZR_WEBSERVICE_EXPORTED = '%s.exported' % LAZR_WEBSERVICE_NS
62COLLECTION_TYPE = 'collection'62COLLECTION_TYPE = 'collection'
@@ -780,8 +780,14 @@
780780
781 def __get__(self, instance, owner):781 def __get__(self, instance, owner):
782 """Look up the entry schema that adapts the model schema."""782 """Look up the entry schema that adapts the model schema."""
783 entry_class = getGlobalSiteManager().adapters.lookup1(783 if instance is None or instance.request is None:
784 self.model_schema, IEntry)784 request = get_current_browser_request()
785 else:
786 request = instance.request
787 request_interface = getUtility(
788 IWebServiceVersion, name=request.version)
789 entry_class = getGlobalSiteManager().adapters.lookup(
790 (self.model_schema, request_interface), IEntry)
785 if entry_class is None:791 if entry_class is None:
786 return None792 return None
787 return EntryAdapterUtility(entry_class).entry_interface793 return EntryAdapterUtility(entry_class).entry_interface
788794
=== modified file 'src/lazr/restful/directives/__init__.py'
--- src/lazr/restful/directives/__init__.py 2009-11-19 15:33:49 +0000
+++ src/lazr/restful/directives/__init__.py 2010-01-12 15:21:22 +0000
@@ -10,15 +10,20 @@
10import martian10import martian
11from zope.component import getSiteManager, getUtility11from zope.component import getSiteManager, getUtility
12from zope.location.interfaces import ILocation12from zope.location.interfaces import ILocation
13from zope.interface import alsoProvides
14from zope.interface.interface import InterfaceClass
13from zope.traversing.browser import AbsoluteURL15from zope.traversing.browser import AbsoluteURL
14from zope.traversing.browser.interfaces import IAbsoluteURL16from zope.traversing.browser.interfaces import IAbsoluteURL
1517
16from lazr.restful.interfaces import (18from lazr.restful.interfaces import (
17 IServiceRootResource, IWebServiceConfiguration, IWebServiceLayer)19 IServiceRootResource, IWebServiceClientRequest, IWebServiceConfiguration,
18from lazr.restful import ServiceRootResource20 IWebServiceLayer, IWebServiceVersion)
21from lazr.restful import (
22 register_versioned_request_utility, ServiceRootResource)
19from lazr.restful.simple import (23from lazr.restful.simple import (
20 BaseWebServiceConfiguration, Publication, Request,24 BaseWebServiceConfiguration, Publication, Request,
21 RootResourceAbsoluteURL)25 RootResourceAbsoluteURL)
26from lazr.restful.utils import make_identifier_safe
2227
2328
24class request_class(martian.Directive):29class request_class(martian.Directive):
@@ -57,6 +62,10 @@
5762
58 This grokker then registers an instance of your subclass as the63 This grokker then registers an instance of your subclass as the
59 singleton configuration object.64 singleton configuration object.
65
66 This grokker also creates marker interfaces for every web service
67 version defined in the configuration, and registers each as an
68 IWebServiceVersion utility.
60 """69 """
61 martian.component(BaseWebServiceConfiguration)70 martian.component(BaseWebServiceConfiguration)
62 martian.directive(request_class)71 martian.directive(request_class)
@@ -84,7 +93,18 @@
84 cls.get_request_user = get_request_user93 cls.get_request_user = get_request_user
8594
86 # Register as utility.95 # Register as utility.
87 getSiteManager().registerUtility(cls(), IWebServiceConfiguration)96 utility = cls()
97 sm = getSiteManager()
98 sm.registerUtility(utility, IWebServiceConfiguration)
99
100 # Create and register marker interfaces for request objects.
101 for version in set(
102 utility.active_versions + [utility.latest_version_uri_prefix]):
103 classname = ("IWebServiceClientRequestVersion" +
104 make_identifier_safe(version))
105 marker_interface = InterfaceClass(
106 classname, (IWebServiceClientRequest,), {})
107 register_versioned_request_utility(marker_interface, version)
88 return True108 return True
89109
90110
91111
=== modified file 'src/lazr/restful/docs/multiversion.txt'
--- src/lazr/restful/docs/multiversion.txt 2009-11-19 16:43:08 +0000
+++ src/lazr/restful/docs/multiversion.txt 2010-01-12 15:21:22 +0000
@@ -5,10 +5,121 @@
5services. Typically these different services represent successive5services. Typically these different services represent successive
6versions of a single web service, improved over time.6versions of a single web service, improved over time.
77
8This test defines three different versions of a web service ('beta',
9'1.0', and 'dev'), all based on the same underlying data model.
10
11Setup
12=====
13
14First, let's set up the web service infrastructure. Doing this first
15will let us create HTTP requests for different versions of the web
16service. The first step is to install the common ZCML used by all
17lazr.restful web services.
18
19 >>> from zope.configuration import xmlconfig
20 >>> zcmlcontext = xmlconfig.string("""
21 ... <configure xmlns="http://namespaces.zope.org/zope">
22 ... <include package="lazr.restful" file="basic-site.zcml"/>
23 ... <utility
24 ... factory="lazr.restful.example.base.filemanager.FileManager" />
25 ... </configure>
26 ... """)
27
28Web service configuration object
29--------------------------------
30
31Here's the web service configuration, which defines the three
32versions: 'beta', '1.0', and 'dev'.
33
34 >>> from lazr.restful import directives
35 >>> from lazr.restful.interfaces import IWebServiceConfiguration
36 >>> from lazr.restful.simple import BaseWebServiceConfiguration
37 >>> from lazr.restful.testing.webservice import WebServiceTestPublication
38
39 >>> class WebServiceConfiguration(BaseWebServiceConfiguration):
40 ... hostname = 'api.multiversion.dev'
41 ... use_https = False
42 ... active_versions = ['beta', '1.0']
43 ... latest_version_uri_prefix = 'dev'
44 ... code_revision = 'test'
45 ... max_batch_size = 100
46 ... view_permission = None
47 ... directives.publication_class(WebServiceTestPublication)
48
49 >>> from grokcore.component.testing import grok_component
50 >>> ignore = grok_component(
51 ... 'WebServiceConfiguration', WebServiceConfiguration)
52
53 >>> from zope.component import getUtility
54 >>> config = getUtility(IWebServiceConfiguration)
55
56URL generation
57--------------
58
59The URL to an entry or collection is different in different versions
60of web service. Not only does every URL includes the version number as
61a path element ("http://api.multiversion.dev/1.0/..."), the name or
62location of an object might change from one version to another.
63
64We implement this in this example web service by defining ILocation
65implementations that retrieve the current browser request and branch
66based on the value of request.version. You'll see this in the
67ContactSet class.
68
69Here, we tell Zope to use Zope's default AbsoluteURL class for
70generating the URLs of objects that implement ILocation. There's no
71multiversion-specific code here.
72
73 >>> from zope.component import getSiteManager
74 >>> from zope.traversing.browser import AbsoluteURL
75 >>> from zope.traversing.browser.interfaces import IAbsoluteURL
76 >>> from zope.location.interfaces import ILocation
77 >>> from lazr.restful.interfaces import IWebServiceLayer
78
79 >>> sm = getSiteManager()
80 >>> sm.registerAdapter(
81 ... AbsoluteURL, (ILocation, IWebServiceLayer),
82 ... provided=IAbsoluteURL)
83
84Defining the request marker interfaces
85--------------------------------------
86
87Every version must have a corresponding subclass of
88IWebServiceClientRequest. Each interface class is registered as a
89named utility implementing IWebServiceVersion. For instance, in the
90example below, the IWebServiceRequest10 class will be registered as
91the IWebServiceVersion utility with the name "1.0".
92
93When a request comes in, lazr.restful figures out which version the
94client is asking for, and tags the request with the appropriate marker
95interface. The utility registrations make it easy to get the marker
96interface for a version, given the version string.
97
98In a real application, these interfaces will be generated and
99registered automatically.
100
101 >>> from lazr.restful.interfaces import IWebServiceClientRequest
102 >>> class IWebServiceRequestBeta(IWebServiceClientRequest):
103 ... pass
104
105 >>> class IWebServiceRequest10(IWebServiceClientRequest):
106 ... pass
107
108 >>> class IWebServiceRequestDev(IWebServiceClientRequest):
109 ... pass
110
111 >>> versions = ((IWebServiceRequestBeta, 'beta'),
112 ... (IWebServiceRequest10, '1.0'),
113 ... (IWebServiceRequestDev, 'dev'))
114
115 >>> from lazr.restful import register_versioned_request_utility
116 >>> for cls, version in versions:
117 ... register_versioned_request_utility(cls, version)
118
8Example model objects119Example model objects
9=====================120=====================
10121
11First let's define the data model. The model in webservice.txt is122Now let's define the data model. The model in webservice.txt is
12pretty complicated; this model will be just complicated enough to123pretty complicated; this model will be just complicated enough to
13illustrate how to publish multiple versions of a web service.124illustrate how to publish multiple versions of a web service.
14125
@@ -18,40 +129,21 @@
18 >>> from zope.interface import Interface, Attribute129 >>> from zope.interface import Interface, Attribute
19 >>> from zope.schema import Bool, Bytes, Int, Text, TextLine, Object130 >>> from zope.schema import Bool, Bytes, Int, Text, TextLine, Object
20131
21 >>> class ITestDataObject(Interface):132 >>> class IContact(Interface):
22 ... """A marker interface for data objects."""
23 ... path = Attribute("The path portion of this object's URL. "
24 ... "Defined here for simplicity of testing.")
25
26 >>> class IContact(ITestDataObject):
27 ... name = TextLine(title=u"Name", required=True)133 ... name = TextLine(title=u"Name", required=True)
28 ... phone = TextLine(title=u"Phone number", required=True)134 ... phone = TextLine(title=u"Phone number", required=True)
29 ... fax = TextLine(title=u"Fax number", required=False)135 ... fax = TextLine(title=u"Fax number", required=False)
30136
31Here's the interface for the 'set' object that manages the contacts.137Here's an interface for the 'set' object that manages the
138contacts.
32139
33 >>> from lazr.restful.interfaces import ITraverseWithGet140 >>> from lazr.restful.interfaces import ITraverseWithGet
34 >>> class IContactSet(ITestDataObject, ITraverseWithGet):141 >>> class IContactSet(ITraverseWithGet):
35 ... def getAll(self):142 ... def getAllContacts():
36 ... "Get all contacts."143 ... "Get all contacts."
37 ...144 ...
38 ... def get(self, name):145 ... def findContacts(self, string, search_fax):
39 ... "Retrieve a single contact by name."146 ... """Find contacts by name, phone number, or fax number."""
40
41Before we can define any classes, a bit of web service setup. Let's
42make all component lookups use the global site manager.
43
44 >>> from zope.component import getSiteManager
45 >>> sm = getSiteManager()
46
47 >>> from zope.component import adapter
48 >>> from zope.component.interfaces import IComponentLookup
49 >>> from zope.interface import implementer, Interface
50 >>> @implementer(IComponentLookup)
51 ... @adapter(Interface)
52 ... def everything_uses_the_global_site_manager(context):
53 ... return sm
54 >>> sm.registerAdapter(everything_uses_the_global_site_manager)
55147
56Here's a simple implementation of IContact.148Here's a simple implementation of IContact.
57149
@@ -59,40 +151,64 @@
59 >>> from zope.interface import implements151 >>> from zope.interface import implements
60 >>> from lazr.restful.security import protect_schema152 >>> from lazr.restful.security import protect_schema
61 >>> class Contact:153 >>> class Contact:
62 ... implements(IContact)154 ... implements(IContact, ILocation)
63 ... def __init__(self, name, phone, fax):155 ... def __init__(self, name, phone, fax):
64 ... self.name = name156 ... self.name = name
65 ... self.phone = phone157 ... self.phone = phone
66 ... self.fax = fax158 ... self.fax = fax
67 ...159 ...
68 ... @property160 ... @property
69 ... def path(self):161 ... def __parent__(self):
70 ... return 'contacts/' + quote(self.name)162 ... return ContactSet()
163 ...
164 ... @property
165 ... def __name__(self):
166 ... return self.name
71 >>> protect_schema(Contact, IContact)167 >>> protect_schema(Contact, IContact)
72168
73Here's a simple ContactSet with a predefined list of contacts.169Here's a simple ContactSet with a predefined list of contacts.
74170
171 >>> from zope.publisher.interfaces.browser import IBrowserRequest
172 >>> from lazr.restful.interfaces import IServiceRootResource
75 >>> from lazr.restful.simple import TraverseWithGet173 >>> from lazr.restful.simple import TraverseWithGet
76 >>> from zope.publisher.interfaces.browser import IBrowserRequest174 >>> from lazr.restful.utils import get_current_browser_request
77 >>> class ContactSet(TraverseWithGet):175 >>> class ContactSet(TraverseWithGet):
78 ... implements(IContactSet)176 ... implements(IContactSet, ILocation)
79 ... path = "contacts"
80 ...177 ...
81 ... def __init__(self):178 ... def __init__(self):
82 ... self.contacts = CONTACTS179 ... self.contacts = CONTACTS
83 ...180 ...
84 ... def get(self, name):181 ... def get(self, request, name):
85 ... contacts = [contact for contacts in self.contacts182 ... contacts = [contact for contact in self.contacts
86 ... if pair.name == name]183 ... if contact.name == name]
87 ... if len(contacts) == 1:184 ... if len(contacts) == 1:
88 ... return contacts[0]185 ... return contacts[0]
89 ... return None186 ... return None
90 ...187 ...
91 ... def getAllContacts(self):188 ... def getAllContacts(self):
92 ... return self.contacts189 ... return self.contacts
190 ...
191 ... def findContacts(self, string, search_fax=True):
192 ... return [contact for contact in self.contacts
193 ... if (string in contact.name
194 ... or string in contact.phone
195 ... or (search_fax and string in contact.fax))]
196 ...
197 ... @property
198 ... def __parent__(self):
199 ... request = get_current_browser_request()
200 ... return getUtility(
201 ... IServiceRootResource, name=request.version)
202 ...
203 ... @property
204 ... def __name__(self):
205 ... request = get_current_browser_request()
206 ... if request.version == 'beta':
207 ... return 'contact_list'
208 ... return 'contacts'
93209
94 >>> sm.registerAdapter(210 >>> from lazr.restful.security import protect_schema
95 ... TraverseWithGet, [ITestDataObject, IBrowserRequest])211 >>> protect_schema(ContactSet, IContactSet)
96212
97Here are the "model objects" themselves:213Here are the "model objects" themselves:
98214
@@ -118,7 +234,7 @@
118 ... """Marker for a contact published through the web service."""234 ... """Marker for a contact published through the web service."""
119235
120 >>> from zope.interface import taggedValue236 >>> from zope.interface import taggedValue
121 >>> from lazr.restful.interfaces import IEntry, LAZR_WEBSERVICE_NAME237 >>> from lazr.restful.interfaces import LAZR_WEBSERVICE_NAME
122 >>> class IContactEntryBeta(IContactEntry, IContact):238 >>> class IContactEntryBeta(IContactEntry, IContact):
123 ... """The part of an author we expose through the web service."""239 ... """The part of an author we expose through the web service."""
124 ... taggedValue(LAZR_WEBSERVICE_NAME,240 ... taggedValue(LAZR_WEBSERVICE_NAME,
@@ -169,14 +285,18 @@
169 ... implements(IContactEntryBeta)285 ... implements(IContactEntryBeta)
170 ... delegates(IContactEntryBeta)286 ... delegates(IContactEntryBeta)
171 ... schema = IContactEntryBeta287 ... schema = IContactEntryBeta
288 ... def __init__(self, context, request):
289 ... self.context = context
290
172 >>> sm.registerAdapter(291 >>> sm.registerAdapter(
173 ... ContactEntryBeta, provided=IContactEntry, name="beta")292 ... ContactEntryBeta, [IContact, IWebServiceRequestBeta],
293 ... provided=IContactEntry)
174294
175By wrapping one of our predefined Contacts in a ContactEntryBeta295By wrapping one of our predefined Contacts in a ContactEntryBeta
176object, we can verify that it implements IContactEntryBeta and296object, we can verify that it implements IContactEntryBeta and
177IContactEntry.297IContactEntry.
178298
179 >>> entry = ContactEntryBeta(C1)299 >>> entry = ContactEntryBeta(C1, None)
180 >>> IContactEntry.validateInvariants(entry)300 >>> IContactEntry.validateInvariants(entry)
181 >>> IContactEntryBeta.validateInvariants(entry)301 >>> IContactEntryBeta.validateInvariants(entry)
182302
@@ -184,24 +304,26 @@
184properties to implement the different field names.304properties to implement the different field names.
185305
186 >>> class ContactEntry10(Entry):306 >>> class ContactEntry10(Entry):
187 ... adapts(IContact)307 ... adapts(IContact)
188 ... implements(IContactEntry10)308 ... implements(IContactEntry10)
189 ... schema = IContactEntry10309 ... delegates(IContactEntry10)
190 ...310 ... schema = IContactEntry10
191 ... def __init__(self, contact):311 ...
192 ... self.contact = contact312 ... def __init__(self, context, request):
193 ...313 ... self.context = context
194 ... @property314 ...
195 ... def phone_number(self):315 ... @property
196 ... return self.contact.phone316 ... def phone_number(self):
197 ...317 ... return self.context.phone
198 ... @property318 ...
199 ... def fax_number(self):319 ... @property
200 ... return self.contact.fax320 ... def fax_number(self):
321 ... return self.context.fax
201 >>> sm.registerAdapter(322 >>> sm.registerAdapter(
202 ... ContactEntry10, provided=IContactEntry, name="1.0")323 ... ContactEntry10, [IContact, IWebServiceRequest10],
324 ... provided=IContactEntry)
203325
204 >>> entry = ContactEntry10(C1)326 >>> entry = ContactEntry10(C1, None)
205 >>> IContactEntry.validateInvariants(entry)327 >>> IContactEntry.validateInvariants(entry)
206 >>> IContactEntry10.validateInvariants(entry)328 >>> IContactEntry10.validateInvariants(entry)
207329
@@ -209,20 +331,22 @@
209the web service.331the web service.
210332
211 >>> class ContactEntryDev(Entry):333 >>> class ContactEntryDev(Entry):
212 ... adapts(IContact)334 ... adapts(IContact)
213 ... implements(IContactEntryDev)335 ... implements(IContactEntryDev)
214 ... schema = IContactEntryDev336 ... delegates(IContactEntryDev)
215 ...337 ... schema = IContactEntryDev
216 ... def __init__(self, contact):338 ...
217 ... self.contact = contact339 ... def __init__(self, context, request):
218 ...340 ... self.context = context
219 ... @property341 ...
220 ... def phone_number(self):342 ... @property
221 ... return self.contact.phone343 ... def phone_number(self):
344 ... return self.context.phone
222 >>> sm.registerAdapter(345 >>> sm.registerAdapter(
223 ... ContactEntryDev, provided=IContactEntry, name="dev")346 ... ContactEntryDev, [IContact, IWebServiceRequestDev],
347 ... provided=IContactEntry)
224348
225 >>> entry = ContactEntryDev(C1)349 >>> entry = ContactEntryDev(C1, None)
226 >>> IContactEntry.validateInvariants(entry)350 >>> IContactEntry.validateInvariants(entry)
227 >>> IContactEntryDev.validateInvariants(entry)351 >>> IContactEntryDev.validateInvariants(entry)
228352
@@ -239,19 +363,35 @@
239 ...363 ...
240 ComponentLookupError: ...364 ComponentLookupError: ...
241365
242When adapting Contact to IEntry you must specify a version number as366When adapting Contact to IEntry you must provide a versioned request
243the name of the adapter. The object you get back will implement the367object. The IEntry object you get back will implement the appropriate
244appropriate version of the web service.368version of the web service.
245369
246 >>> beta_entry = getAdapter(C1, IEntry, name="beta")370To test this we'll need to manually create some versioned request
371objects. The traversal process would take care of this for us (see
372"Request lifecycle" below), but it won't work yet because we have yet
373to define a service root resource.
374
375 >>> from lazr.restful.testing.webservice import (
376 ... create_web_service_request)
377 >>> from zope.interface import alsoProvides
378
379 >>> from zope.component import getMultiAdapter
380 >>> request_beta = create_web_service_request('/beta/')
381 >>> alsoProvides(request_beta, IWebServiceRequestBeta)
382 >>> beta_entry = getMultiAdapter((C1, request_beta), IEntry)
247 >>> print beta_entry.fax383 >>> print beta_entry.fax
248 111-2121384 111-2121
249385
250 >>> one_oh_entry = getAdapter(C1, IEntry, name="1.0")386 >>> request_10 = create_web_service_request('/1.0/')
387 >>> alsoProvides(request_10, IWebServiceRequest10)
388 >>> one_oh_entry = getMultiAdapter((C1, request_10), IEntry)
251 >>> print one_oh_entry.fax_number389 >>> print one_oh_entry.fax_number
252 111-2121390 111-2121
253391
254 >>> dev_entry = getAdapter(C1, IEntry, name="dev")392 >>> request_dev = create_web_service_request('/dev/')
393 >>> alsoProvides(request_dev, IWebServiceRequestDev)
394 >>> dev_entry = getMultiAdapter((C1, request_dev), IEntry)
255 >>> print dev_entry.fax395 >>> print dev_entry.fax
256 Traceback (most recent call last):396 Traceback (most recent call last):
257 ...397 ...
@@ -260,9 +400,13 @@
260Implementing the collection resource400Implementing the collection resource
261====================================401====================================
262402
263The contact collection itself doesn't change between versions (though403The set of contacts publishes a slightly different named operation in
264it could). We'll define it once and register it for every version of404every version of the web service, so in a little bit we'll be
265the web service.405implementing three different versions of the same named operation. But
406the contact set itself doesn't change between versions (although it
407could). So it's sufficient to implement one ICollection implementation
408and register it as the implementation for every version of the web
409service.
266410
267 >>> from lazr.restful import Collection411 >>> from lazr.restful import Collection
268 >>> from lazr.restful.interfaces import ICollection412 >>> from lazr.restful.interfaces import ICollection
@@ -277,67 +421,87 @@
277 ... """Find all the contacts."""421 ... """Find all the contacts."""
278 ... return self.context.getAllContacts()422 ... return self.context.getAllContacts()
279423
280Let's make sure ContactCollection implements ICollection.424Let's make sure it implements ICollection.
281425
282 >>> from zope.interface.verify import verifyObject426 >>> from zope.interface.verify import verifyObject
283 >>> contact_set = ContactSet()427 >>> contact_set = ContactSet()
284 >>> verifyObject(ICollection, ContactCollection(contact_set))428 >>> verifyObject(ICollection, ContactCollection(contact_set, None))
285 True429 True
286430
287Once we register ContactCollection as the ICollection implementation,431Register it as the ICollection adapter for IContactSet. We use a
288we can adapt the ContactSet object to a web service ICollection.432generic request interface (IWebServiceClientRequest) rather than a
289433specific one like IWebServiceRequestBeta, so that the same
290 >>> for version in ['beta', 'dev', '1.0']:434implementation will be used for every version of the web service.
291 ... sm.registerAdapter(435
292 ... ContactCollection, provided=ICollection, name=version)436 >>> sm.registerAdapter(
293437 ... ContactCollection, [IContactSet, IWebServiceClientRequest],
294 >>> dev_collection = getAdapter(contact_set, ICollection, name="dev")438 ... provided=ICollection)
295 >>> len(dev_collection.find())439
296 2440Make sure the functionality works properly.
297441
298 >>> dev_collection = getAdapter(contact_set, ICollection, name="dev")442 >>> collection = getMultiAdapter(
299 >>> len(dev_collection.find())443 ... (contact_set, request_beta), ICollection)
300 2444 >>> len(collection.find())
301445 2
302Web service infrastructure initialization446
303=========================================447Implementing the named operations
304448---------------------------------
305Now that we've defined the data model, it's time to set up the web449
306service infrastructure.450All three versions of the web service publish a named operation for
307451searching for contacts, but they publish it in slightly different
308 >>> from zope.configuration import xmlconfig452ways. In 'beta' it publishes a named operation called 'findContacts',
309 >>> zcmlcontext = xmlconfig.string("""453which does a search based on name, phone number, and fax number. In
310 ... <configure xmlns="http://namespaces.zope.org/zope">454'1.0' it publishes the same operation, but the name is
311 ... <include package="lazr.restful" file="basic-site.zcml"/>455'find'. In 'dev' the contact set publishes 'find',
312 ... <utility456but the functionality is changed to search only the name and phone
313 ... factory="lazr.restful.example.base.filemanager.FileManager" />457number.
314 ... </configure>458
315 ... """)459Here's the named operation as implemented in versions 'beta' and '1.0'.
316460
317Here's the configuration, which defines the three versions: 'beta',461 >>> from lazr.restful import ResourceGETOperation
318'1.0', and 'dev'.462 >>> from lazr.restful.fields import CollectionField, Reference
319463 >>> from lazr.restful.interfaces import IResourceGETOperation
320 >>> from lazr.restful import directives464 >>> class FindContactsOperationBase(ResourceGETOperation):
321 >>> from lazr.restful.interfaces import IWebServiceConfiguration465 ... """An operation that searches for contacts."""
322 >>> from lazr.restful.simple import BaseWebServiceConfiguration466 ... implements(IResourceGETOperation)
323 >>> from lazr.restful.testing.webservice import WebServiceTestPublication467 ...
324468 ... params = [ TextLine(__name__='string') ]
325 >>> class WebServiceConfiguration(BaseWebServiceConfiguration):469 ... return_type = CollectionField(value_type=Reference(schema=IContact))
326 ... hostname = 'api.multiversion.dev'470 ...
327 ... use_https = False471 ... def call(self, string):
328 ... active_versions = ['beta', '1.0']472 ... try:
329 ... latest_version_uri_prefix = 'dev'473 ... return self.context.findContacts(string)
330 ... code_revision = 'test'474 ... except ValueError, e:
331 ... max_batch_size = 100475 ... self.request.response.setStatus(400)
332 ... directives.publication_class(WebServiceTestPublication)476 ... return str(e)
333477
334 >>> from grokcore.component.testing import grok_component478This operation is registered as the "findContacts" operation in the
335 >>> ignore = grok_component(479'beta' service, and the 'find' operation in the '1.0' service.
336 ... 'WebServiceConfiguration', WebServiceConfiguration)480
337481 >>> sm.registerAdapter(
338 >>> from zope.component import getUtility482 ... FindContactsOperationBase, [IContactSet, IWebServiceRequestBeta],
339 >>> config = getUtility(IWebServiceConfiguration)483 ... provided=IResourceGETOperation, name="findContacts")
340484
485 >>> sm.registerAdapter(
486 ... FindContactsOperationBase, [IContactSet, IWebServiceRequest10],
487 ... provided=IResourceGETOperation, name="find")
488
489Here's the slightly different named operation as implemented in
490version 'dev'.
491
492 >>> class FindContactsOperationNoFax(FindContactsOperationBase):
493 ... """An operation that searches for contacts."""
494 ...
495 ... def call(self, string):
496 ... try:
497 ... return self.context.findContacts(string, False)
498 ... except ValueError, e:
499 ... self.request.response.setStatus(400)
500 ... return str(e)
501
502 >>> sm.registerAdapter(
503 ... FindContactsOperationNoFax, [IContactSet, IWebServiceRequestDev],
504 ... provided=IResourceGETOperation, name="find")
341505
342The service root resource506The service root resource
343=========================507=========================
@@ -346,19 +510,20 @@
346roots. The 'beta' web service will publish the contact set as510roots. The 'beta' web service will publish the contact set as
347'contact_list', and subsequent versions will publish it as 'contacts'.511'contact_list', and subsequent versions will publish it as 'contacts'.
348512
349 >>> from lazr.restful.interfaces import IServiceRootResource
350 >>> from lazr.restful.simple import RootResource513 >>> from lazr.restful.simple import RootResource
351 >>> from zope.traversing.browser.interfaces import IAbsoluteURL514 >>> from zope.traversing.browser.interfaces import IAbsoluteURL
352515
353 >>> class BetaServiceRootResource(RootResource):516 >>> class BetaServiceRootResource(RootResource):
354 ... implements(IAbsoluteURL)517 ... implements(IAbsoluteURL)
355 ...518 ...
356 ... top_level_objects = { 'contact_list': ContactSet() }519 ... top_level_collections = {
520 ... 'contact_list': (IContact, ContactSet()) }
357521
358 >>> class PostBetaServiceRootResource(RootResource):522 >>> class PostBetaServiceRootResource(RootResource):
359 ... implements(IAbsoluteURL)523 ... implements(IAbsoluteURL)
360 ...524 ...
361 ... top_level_objects = { 'contacts': ContactSet() }525 ... top_level_collections = {
526 ... 'contacts': (IContact, ContactSet()) }
362527
363 >>> for version, cls in (('beta', BetaServiceRootResource),528 >>> for version, cls in (('beta', BetaServiceRootResource),
364 ... ('1.0', PostBetaServiceRootResource),529 ... ('1.0', PostBetaServiceRootResource),
@@ -378,22 +543,338 @@
378Both classes will use the default lazr.restful code to generate their543Both classes will use the default lazr.restful code to generate their
379URLs.544URLs.
380545
546 >>> from zope.traversing.browser import absoluteURL
381 >>> from lazr.restful.simple import RootResourceAbsoluteURL547 >>> from lazr.restful.simple import RootResourceAbsoluteURL
382 >>> for cls in (BetaServiceRootResource, PostBetaServiceRootResource):548 >>> for cls in (BetaServiceRootResource, PostBetaServiceRootResource):
383 ... sm.registerAdapter(549 ... sm.registerAdapter(
384 ... RootResourceAbsoluteURL, [cls, IBrowserRequest])550 ... RootResourceAbsoluteURL, [cls, IBrowserRequest])
385551
386 >>> from zope.traversing.browser import absoluteURL
387 >>> from lazr.restful.testing.webservice import (
388 ... create_web_service_request)
389
390 >>> beta_request = create_web_service_request('/beta/')552 >>> beta_request = create_web_service_request('/beta/')
391 >>> ignore = beta_request.traverse(None)553 >>> print beta_request.traverse(None)
554 <BetaServiceRootResource object...>
555
392 >>> print absoluteURL(beta_app, beta_request)556 >>> print absoluteURL(beta_app, beta_request)
393 http://api.multiversion.dev/beta/557 http://api.multiversion.dev/beta/
394558
395 >>> dev_request = create_web_service_request('/dev/')559 >>> dev_request = create_web_service_request('/dev/')
396 >>> ignore = dev_request.traverse(None)560 >>> print dev_request.traverse(None)
561 <PostBetaServiceRootResource object...>
562
397 >>> print absoluteURL(dev_app, dev_request)563 >>> print absoluteURL(dev_app, dev_request)
398 http://api.multiversion.dev/dev/564 http://api.multiversion.dev/dev/
399565
566Request lifecycle
567=================
568
569When a request first comes in, there's no way to tell which version
570it's associated with.
571
572 >>> from lazr.restful.testing.webservice import (
573 ... create_web_service_request)
574
575 >>> request_beta = create_web_service_request('/beta/')
576 >>> IWebServiceRequestBeta.providedBy(request_beta)
577 False
578
579The traversal process associates the request with a particular version.
580
581 >>> request_beta.traverse(None)
582 <BetaServiceRootResource object ...>
583 >>> IWebServiceRequestBeta.providedBy(request_beta)
584 True
585 >>> print request_beta.version
586 beta
587
588Using the web service
589=====================
590
591Now that we can create versioned web service requests, let's try out
592the different versions of the web service.
593
594Beta
595----
596
597Here's the service root resource.
598
599 >>> import simplejson
600 >>> request = create_web_service_request('/beta/')
601 >>> resource = request.traverse(None)
602 >>> body = simplejson.loads(resource())
603 >>> print sorted(body.keys())
604 ['contacts_collection_link', 'resource_type_link']
605
606 >>> print body['contacts_collection_link']
607 http://api.multiversion.dev/beta/contact_list
608
609Here's the contact list.
610
611 >>> request = create_web_service_request('/beta/contact_list')
612 >>> resource = request.traverse(None)
613
614We can't access the underlying data model object through the request,
615but since we happen to know which object it is, we can pass it into
616absoluteURL along with the request object, and get the correct URL.
617
618 >>> print absoluteURL(contact_set, request)
619 http://api.multiversion.dev/beta/contact_list
620
621 >>> body = simplejson.loads(resource())
622 >>> body['total_size']
623 2
624 >>> for link in sorted(
625 ... [contact['self_link'] for contact in body['entries']]):
626 ... print link
627 http://api.multiversion.dev/beta/contact_list/Cleo%20Python
628 http://api.multiversion.dev/beta/contact_list/Oliver%20Bluth
629
630We can traverse through the collection to an entry.
631
632 >>> request_beta = create_web_service_request(
633 ... '/beta/contact_list/Cleo Python')
634 >>> resource = request_beta.traverse(None)
635
636Again, we can't access the underlying data model object through the
637request, but since we know which object represents Cleo Python, we can
638pass it into absoluteURL along with this request object, and get the
639object's URL.
640
641 >>> print C1.name
642 Cleo Python
643 >>> print absoluteURL(C1, request_beta)
644 http://api.multiversion.dev/beta/contact_list/Cleo%20Python
645
646 >>> body = simplejson.loads(resource())
647 >>> sorted(body.keys())
648 ['fax', 'http_etag', 'name', 'phone', 'resource_type_link', 'self_link']
649 >>> print body['name']
650 Cleo Python
651
652We can traverse through an entry to one of its fields.
653
654 >>> request_beta = create_web_service_request(
655 ... '/beta/contact_list/Cleo Python/fax')
656 >>> field = request_beta.traverse(None)
657 >>> print simplejson.loads(field())
658 111-2121
659
660We can invoke a named operation.
661
662 >>> import simplejson
663 >>> request_beta = create_web_service_request(
664 ... '/beta/contact_list',
665 ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=Cleo'})
666 >>> operation = request_beta.traverse(None)
667 >>> result = simplejson.loads(operation())
668 >>> [contact['name'] for contact in result['entries']]
669 ['Cleo Python']
670
671 >>> request_beta = create_web_service_request(
672 ... '/beta/contact_list',
673 ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=111'})
674
675 >>> operation = request_beta.traverse(None)
676 >>> result = simplejson.loads(operation())
677 >>> [contact['fax'] for contact in result['entries']]
678 ['111-2121']
679
6801.0
681---
682
683Here's the service root resource.
684
685 >>> import simplejson
686 >>> request = create_web_service_request('/1.0/')
687 >>> resource = request.traverse(None)
688 >>> body = simplejson.loads(resource())
689 >>> print sorted(body.keys())
690 ['contacts_collection_link', 'resource_type_link']
691
692Note that 'contacts_collection_link' points to a different URL in
693'1.0' than in 'dev'.
694
695 >>> print body['contacts_collection_link']
696 http://api.multiversion.dev/1.0/contacts
697
698An attempt to use the 'beta' name of the contact list in the '1.0' web
699service will fail.
700
701 >>> request = create_web_service_request('/1.0/contact_list')
702 >>> resource = request.traverse(None)
703 Traceback (most recent call last):
704 ...
705 NotFound: Object: <PostBetaServiceRootResource...>, name: u'contact_list'
706
707Here's the contact list under its correct URL.
708
709 >>> request = create_web_service_request('/1.0/contacts')
710 >>> resource = request.traverse(None)
711 >>> print absoluteURL(contact_set, request)
712 http://api.multiversion.dev/1.0/contacts
713
714 >>> body = simplejson.loads(resource())
715 >>> body['total_size']
716 2
717 >>> for link in sorted(
718 ... [contact['self_link'] for contact in body['entries']]):
719 ... print link
720 http://api.multiversion.dev/1.0/contacts/Cleo%20Python
721 http://api.multiversion.dev/1.0/contacts/Oliver%20Bluth
722
723We can traverse through the collection to an entry.
724
725 >>> request_10 = create_web_service_request(
726 ... '/1.0/contacts/Cleo Python')
727 >>> resource = request_10.traverse(None)
728 >>> print absoluteURL(C1, request_10)
729 http://api.multiversion.dev/1.0/contacts/Cleo%20Python
730
731Note that the 'fax' and 'phone' fields are now called 'fax_number' and
732'phone_number'.
733
734 >>> body = simplejson.loads(resource())
735 >>> sorted(body.keys())
736 ['fax_number', 'http_etag', 'name', 'phone_number',
737 'resource_type_link', 'self_link']
738 >>> print body['name']
739 Cleo Python
740
741We can traverse through an entry to one of its fields.
742
743 >>> request_10 = create_web_service_request(
744 ... '/1.0/contacts/Cleo Python/fax_number')
745 >>> field = request_10.traverse(None)
746 >>> print simplejson.loads(field())
747 111-2121
748
749The fax field in '1.0' is called 'fax_number', and attempting
750to traverse to its 'beta' name ('fax') will fail.
751
752 >>> request_10 = create_web_service_request(
753 ... '/1.0/contacts/Cleo Python/fax')
754 >>> field = request_10.traverse(None)
755 Traceback (most recent call last):
756 ...
757 NotFound: Object: <Contact object...>, name: u'fax'
758
759We can invoke a named operation. Note that the name of the operation
760is now 'find' (it was 'findContacts' in 'beta').
761
762 >>> request_10 = create_web_service_request(
763 ... '/1.0/contacts',
764 ... environ={'QUERY_STRING' : 'ws.op=find&string=Cleo'})
765 >>> operation = request_10.traverse(None)
766 >>> result = simplejson.loads(operation())
767 >>> [contact['name'] for contact in result['entries']]
768 ['Cleo Python']
769
770 >>> request_10 = create_web_service_request(
771 ... '/1.0/contacts',
772 ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})
773 >>> operation = request_10.traverse(None)
774 >>> result = simplejson.loads(operation())
775 >>> [contact['fax_number'] for contact in result['entries']]
776 ['111-2121']
777
778Attempting to invoke the operation using its 'beta' name won't work.
779
780 >>> request_10 = create_web_service_request(
781 ... '/1.0/contacts',
782 ... environ={'QUERY_STRING' : 'ws.op=findContacts&string=Cleo'})
783 >>> operation = request_10.traverse(None)
784 >>> print operation()
785 No such operation: findContacts
786
787Dev
788---
789
790Here's the service root resource.
791
792 >>> request = create_web_service_request('/dev/')
793 >>> resource = request.traverse(None)
794 >>> body = simplejson.loads(resource())
795 >>> print sorted(body.keys())
796 ['contacts_collection_link', 'resource_type_link']
797
798 >>> print body['contacts_collection_link']
799 http://api.multiversion.dev/dev/contacts
800
801Here's the contact list.
802
803 >>> request_dev = create_web_service_request('/dev/contacts')
804 >>> resource = request_dev.traverse(None)
805 >>> print absoluteURL(contact_set, request_dev)
806 http://api.multiversion.dev/dev/contacts
807
808 >>> body = simplejson.loads(resource())
809 >>> body['total_size']
810 2
811 >>> for link in sorted(
812 ... [contact['self_link'] for contact in body['entries']]):
813 ... print link
814 http://api.multiversion.dev/dev/contacts/Cleo%20Python
815 http://api.multiversion.dev/dev/contacts/Oliver%20Bluth
816
817We can traverse through the collection to an entry.
818
819 >>> request_dev = create_web_service_request(
820 ... '/dev/contacts/Cleo Python')
821 >>> resource = request_dev.traverse(None)
822 >>> print absoluteURL(C1, request_dev)
823 http://api.multiversion.dev/dev/contacts/Cleo%20Python
824
825Note that the published field names have changed between 'dev' and
826'1.0'. The phone field is still 'phone_number', but the 'fax_number'
827field is gone.
828
829 >>> body = simplejson.loads(resource())
830 >>> sorted(body.keys())
831 ['http_etag', 'name', 'phone_number', 'resource_type_link', 'self_link']
832 >>> print body['name']
833 Cleo Python
834
835We can traverse through an entry to one of its fields.
836
837 >>> request_dev = create_web_service_request(
838 ... '/dev/contacts/Cleo Python/name')
839 >>> field = request_dev.traverse(None)
840 >>> print simplejson.loads(field())
841 Cleo Python
842
843We cannot use 'dev' to traverse to a field not published in the 'dev'
844version.
845
846 >>> request_beta = create_web_service_request(
847 ... '/dev/contacts/Cleo Python/fax')
848 >>> field = request_beta.traverse(None)
849 Traceback (most recent call last):
850 ...
851 NotFound: Object: <Contact object...>, name: u'fax'
852
853 >>> request_beta = create_web_service_request(
854 ... '/dev/contacts/Cleo Python/fax_number')
855 >>> field = request_beta.traverse(None)
856 Traceback (most recent call last):
857 ...
858 NotFound: Object: <Contact object...>, name: u'fax_number'
859
860We can invoke a named operation.
861
862 >>> request_dev = create_web_service_request(
863 ... '/dev/contacts',
864 ... environ={'QUERY_STRING' : 'ws.op=find&string=Cleo'})
865 >>> operation = request_dev.traverse(None)
866 >>> result = simplejson.loads(operation())
867 >>> [contact['name'] for contact in result['entries']]
868 ['Cleo Python']
869
870Note that a search for Cleo's fax number no longer finds anything,
871because the named operation published as 'find' in the 'dev' web
872service doesn't search the fax field.
873
874 >>> request_dev = create_web_service_request(
875 ... '/dev/contacts',
876 ... environ={'QUERY_STRING' : 'ws.op=find&string=111'})
877 >>> operation = request_dev.traverse(None)
878 >>> result = simplejson.loads(operation())
879 >>> result['total_size']
880 0
400881
=== modified file 'src/lazr/restful/docs/utils.txt'
--- src/lazr/restful/docs/utils.txt 2009-08-27 15:50:55 +0000
+++ src/lazr/restful/docs/utils.txt 2010-01-12 15:21:22 +0000
@@ -75,8 +75,33 @@
75 >>> print implementation().a_method()75 >>> print implementation().a_method()
76 superclass result76 superclass result
7777
7878make_identifier_safe
79=================================79====================
80
81LAZR provides a way of converting an arbitrary string into a similar
82string that can be used as a Python identifier.
83
84 >>> from lazr.restful.utils import make_identifier_safe
85 >>> print make_identifier_safe("already_a_valid_IDENTIFIER_444")
86 already_a_valid_IDENTIFIER_444
87
88 >>> print make_identifier_safe("!starts_with_punctuation")
89 _starts_with_punctuation
90
91 >>> print make_identifier_safe("_!contains!pu-nc.tuation")
92 __contains_pu_nc_tuation
93
94 >>> print make_identifier_safe("contains\nnewline")
95 contains_newline
96
97 >>> print make_identifier_safe("")
98 _
99
100 >>> print make_identifier_safe(None)
101 Traceback (most recent call last):
102 ...
103 ValueError: Cannot make None value identifier-safe.
104
80camelcase_to_underscore_separated105camelcase_to_underscore_separated
81=================================106=================================
82107
@@ -97,7 +122,6 @@
97 >>> camelcase_to_underscore_separated('_StartsWithUnderscore')122 >>> camelcase_to_underscore_separated('_StartsWithUnderscore')
98 '__starts_with_underscore'123 '__starts_with_underscore'
99124
100==============
101safe_hasattr()125safe_hasattr()
102==============126==============
103127
@@ -130,7 +154,6 @@
130 >>> safe_hasattr(oracle, 'weather')154 >>> safe_hasattr(oracle, 'weather')
131 False155 False
132156
133============
134smartquote()157smartquote()
135============158============
136159
@@ -155,7 +178,6 @@
155 >>> smartquote('a lot of "foo"?')178 >>> smartquote('a lot of "foo"?')
156 u'a lot of \u201cfoo\u201d?'179 u'a lot of \u201cfoo\u201d?'
157180
158================
159safe_js_escape()181safe_js_escape()
160================182================
161183
162184
=== modified file 'src/lazr/restful/docs/webservice-declarations.txt'
--- src/lazr/restful/docs/webservice-declarations.txt 2009-11-16 14:49:53 +0000
+++ src/lazr/restful/docs/webservice-declarations.txt 2010-01-12 15:21:22 +0000
@@ -867,8 +867,50 @@
867 ... self.base_price = base_price867 ... self.base_price = base_price
868 ... self.inventory_number = inventory_number868 ... self.inventory_number = inventory_number
869869
870Before we can continue, we must define a web service configuration
871object. Each web service needs to have one of these registered
872utilities providing basic information about the web service. This one
873is just a dummy.
874
875 >>> from zope.component import provideUtility
876 >>> from lazr.restful.interfaces import IWebServiceConfiguration
877 >>> class MyWebServiceConfiguration:
878 ... implements(IWebServiceConfiguration)
879 ... view_permission = "lazr.View"
880 ... active_versions = ["beta"]
881 ... code_revision = "1.0b"
882 ... default_batch_size = 50
883 ... latest_version_uri_prefix = 'beta'
884 ...
885 ... def get_request_user(self):
886 ... return 'A user'
887 >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)
888
889
890We must also set up the ability to create versioned requests. We only
891have one version of the web service ('beta'), but lazr.restful
892requires every request to be marked with a version string and to
893implement an appropriate marker interface. Here, we define the marker
894interface for the 'beta' version of the web service.
895
896 >>> from zope.component import getSiteManager
897 >>> from lazr.restful.interfaces import IWebServiceVersion
898 >>> class ITestServiceRequestBeta(IWebServiceVersion):
899 ... pass
900 >>> sm = getSiteManager()
901 >>> sm.registerUtility(
902 ... ITestServiceRequestBeta, IWebServiceVersion,
903 ... name='beta')
904
905 >>> from lazr.restful.testing.webservice import FakeRequest
906 >>> request = FakeRequest()
907
908Now we can turn a Book object into something that implements
909IBookEntry.
910
870 >>> entry_adapter = entry_adapter_factory(911 >>> entry_adapter = entry_adapter_factory(
871 ... Book(u'Aldous Huxley', u'Island', 10.0, '12345'))912 ... Book(u'Aldous Huxley', u'Island', 10.0, '12345'),
913 ... request)
872914
873 >>> entry_adapter.schema is entry_interface915 >>> entry_adapter.schema is entry_interface
874 True916 True
@@ -926,7 +968,7 @@
926 ... return self.books968 ... return self.books
927969
928 >>> collection_adapter = collection_adapter_factory(970 >>> collection_adapter = collection_adapter_factory(
929 ... BookSet(['A book', 'Another book']))971 ... BookSet(['A book', 'Another book']), request)
930972
931 >>> verifyObject(ICollection, collection_adapter)973 >>> verifyObject(ICollection, collection_adapter)
932 True974 True
@@ -943,24 +985,6 @@
943find(). The REQUEST_USER marker value will be replaced by the logged in985find(). The REQUEST_USER marker value will be replaced by the logged in
944user.986user.
945987
946To get this to work we must define a web service configuration
947object. Each web service needs to have one of these registered
948utilities providing basic information about the web service. This one
949is just a dummy.
950
951 >>> from zope.component import provideUtility
952 >>> from lazr.restful.interfaces import IWebServiceConfiguration
953 >>> class MyWebServiceConfiguration:
954 ... implements(IWebServiceConfiguration)
955 ... view_permission = "lazr.View"
956 ... active_versions = ["beta"]
957 ... code_revision = "1.0b"
958 ... default_batch_size = 50
959 ...
960 ... def get_request_user(self):
961 ... return 'A user'
962 >>> provideUtility(MyWebServiceConfiguration(), IWebServiceConfiguration)
963
964 >>> class CheckedOutBookSet(object):988 >>> class CheckedOutBookSet(object):
965 ... """Simple ICheckedOutBookSet implementation."""989 ... """Simple ICheckedOutBookSet implementation."""
966 ... implements(ICheckedOutBookSet)990 ... implements(ICheckedOutBookSet)
@@ -970,7 +994,7 @@
970 ... user, title)994 ... user, title)
971995
972 >>> checked_out_adapter = generate_collection_adapter(996 >>> checked_out_adapter = generate_collection_adapter(
973 ... ICheckedOutBookSet)(CheckedOutBookSet())997 ... ICheckedOutBookSet)(CheckedOutBookSet(), request)
974998
975 >>> checked_out_adapter.find()999 >>> checked_out_adapter.find()
976 A user searched for checked out book matching "".1000 A user searched for checked out book matching "".
@@ -1046,7 +1070,6 @@
10461070
1047Now we can create a fake request that invokes the named operation.1071Now we can create a fake request that invokes the named operation.
10481072
1049 >>> from lazr.restful.testing.webservice import FakeRequest
1050 >>> request = FakeRequest()1073 >>> request = FakeRequest()
1051 >>> read_method_adapter = read_method_adapter_factory(1074 >>> read_method_adapter = read_method_adapter_factory(
1052 ... BookSetOnSteroids(), request)1075 ... BookSetOnSteroids(), request)
@@ -1298,7 +1321,7 @@
1298 ... IHasText, hastext_entry_interface)1321 ... IHasText, hastext_entry_interface)
12991322
1300 >>> obj = HasText()1323 >>> obj = HasText()
1301 >>> hastext_entry_adapter = hastext_entry_adapter_factory(obj)1324 >>> hastext_entry_adapter = hastext_entry_adapter_factory(obj, request)
13021325
1303...and you'll have an object that invokes set_text() when you set the1326...and you'll have an object that invokes set_text() when you set the
1304'text' attribute.1327'text' attribute.
@@ -1531,11 +1554,11 @@
1531After the registration, adapters from IBook to IEntry, and IBookSet to1554After the registration, adapters from IBook to IEntry, and IBookSet to
1532ICollection are available:1555ICollection are available:
15331556
1534 >>> from zope.component import getAdapter1557 >>> from zope.component import getMultiAdapter
1535 >>> book = Book(u'George Orwell', u'1984', 10.0, u'12345-1984')1558 >>> book = Book(u'George Orwell', u'1984', 10.0, u'12345-1984')
1536 >>> bookset = BookSet([book])1559 >>> bookset = BookSet([book])
15371560
1538 >>> entry_adapter = getAdapter(book, IEntry)1561 >>> entry_adapter = getMultiAdapter((book, request), IEntry)
1539 >>> verifyObject(IEntry, entry_adapter)1562 >>> verifyObject(IEntry, entry_adapter)
1540 True1563 True
15411564
@@ -1544,7 +1567,7 @@
1544 >>> verifyObject(entry_adapter.schema, entry_adapter)1567 >>> verifyObject(entry_adapter.schema, entry_adapter)
1545 True1568 True
15461569
1547 >>> collection_adapter = getAdapter(bookset, ICollection)1570 >>> collection_adapter = getMultiAdapter((bookset, request), ICollection)
1548 >>> verifyObject(ICollection, collection_adapter)1571 >>> verifyObject(ICollection, collection_adapter)
1549 True1572 True
15501573
15511574
=== modified file 'src/lazr/restful/docs/webservice-error.txt'
--- src/lazr/restful/docs/webservice-error.txt 2009-08-04 19:27:13 +0000
+++ src/lazr/restful/docs/webservice-error.txt 2010-01-12 15:21:22 +0000
@@ -15,6 +15,7 @@
15 >>> class SimpleWebServiceConfiguration:15 >>> class SimpleWebServiceConfiguration:
16 ... implements(IWebServiceConfiguration)16 ... implements(IWebServiceConfiguration)
17 ... show_tracebacks = False17 ... show_tracebacks = False
18 ... latest_version_uri_prefix = 'trunk'
18 >>> webservice_configuration = SimpleWebServiceConfiguration()19 >>> webservice_configuration = SimpleWebServiceConfiguration()
19 >>> getSiteManager().registerUtility(webservice_configuration)20 >>> getSiteManager().registerUtility(webservice_configuration)
2021
2122
=== modified file 'src/lazr/restful/docs/webservice.txt'
--- src/lazr/restful/docs/webservice.txt 2009-11-19 16:28:38 +0000
+++ src/lazr/restful/docs/webservice.txt 2010-01-12 15:21:22 +0000
@@ -114,23 +114,14 @@
114114
115 >>> from urllib import quote115 >>> from urllib import quote
116 >>> from zope.component import (116 >>> from zope.component import (
117 ... adapts, adapter, getSiteManager, getMultiAdapter)117 ... adapts, getSiteManager, getMultiAdapter)
118 >>> from zope.interface import implements, implementer, Interface118 >>> from zope.interface import implements
119 >>> from zope.publisher.interfaces import IPublishTraverse, NotFound119 >>> from zope.publisher.interfaces import IPublishTraverse, NotFound
120 >>> from zope.publisher.interfaces.browser import IBrowserRequest120 >>> from zope.publisher.interfaces.browser import IBrowserRequest
121 >>> from zope.security.checker import CheckerPublic121 >>> from zope.security.checker import CheckerPublic
122 >>> from zope.traversing.browser.interfaces import IAbsoluteURL122 >>> from zope.traversing.browser.interfaces import IAbsoluteURL
123 >>> from lazr.restful.security import protect_schema123 >>> from lazr.restful.security import protect_schema
124124
125 >>> from zope.component.interfaces import IComponentLookup
126 >>> sm = getSiteManager()
127
128 >>> @implementer(IComponentLookup)
129 ... @adapter(Interface)
130 ... def everything_uses_the_global_site_manager(context):
131 ... return sm
132 >>> sm.registerAdapter(everything_uses_the_global_site_manager)
133
134 >>> class BaseAbsoluteURL:125 >>> class BaseAbsoluteURL:
135 ... """A basic, extensible implementation of IAbsoluteURL."""126 ... """A basic, extensible implementation of IAbsoluteURL."""
136 ... implements(IAbsoluteURL)127 ... implements(IAbsoluteURL)
@@ -143,6 +134,8 @@
143 ... return "http://api.cookbooks.dev/beta/" + self.context.path134 ... return "http://api.cookbooks.dev/beta/" + self.context.path
144 ...135 ...
145 ... __call__ = __str__136 ... __call__ = __str__
137
138 >>> sm = getSiteManager()
146 >>> sm.registerAdapter(139 >>> sm.registerAdapter(
147 ... BaseAbsoluteURL, [ITestDataObject, IBrowserRequest],140 ... BaseAbsoluteURL, [ITestDataObject, IBrowserRequest],
148 ... IAbsoluteURL)141 ... IAbsoluteURL)
@@ -520,6 +513,26 @@
520 >>> from zope.component import getUtility513 >>> from zope.component import getUtility
521 >>> webservice_configuration = getUtility(IWebServiceConfiguration)514 >>> webservice_configuration = getUtility(IWebServiceConfiguration)
522515
516We also need to define a marker interface for each version of the web
517service, so that incoming requests can be marked with the appropriate
518version string. The configuration above defines two versions, 'beta'
519and 'devel'.
520
521 >>> from lazr.restful.interfaces import IWebServiceClientRequest
522 >>> class IWebServiceRequestBeta(IWebServiceClientRequest):
523 ... pass
524
525 >>> class IWebServiceRequestDevel(IWebServiceClientRequest):
526 ... pass
527
528 >>> versions = ((IWebServiceRequestBeta, 'beta'),
529 ... (IWebServiceRequestDevel, 'devel'))
530
531 >>> from lazr.restful import register_versioned_request_utility
532 >>> for cls, version in versions:
533 ... register_versioned_request_utility(cls, version)
534
535
523======================536======================
524Defining the resources537Defining the resources
525======================538======================
@@ -625,6 +638,7 @@
625 >>> from zope.interface.verify import verifyObject638 >>> from zope.interface.verify import verifyObject
626 >>> from lazr.delegates import delegates639 >>> from lazr.delegates import delegates
627 >>> from lazr.restful import Entry640 >>> from lazr.restful import Entry
641 >>> from lazr.restful.testing.webservice import FakeRequest
628642
629 >>> class AuthorEntry(Entry):643 >>> class AuthorEntry(Entry):
630 ... """An author, as exposed through the web service."""644 ... """An author, as exposed through the web service."""
@@ -632,7 +646,8 @@
632 ... delegates(IAuthorEntry)646 ... delegates(IAuthorEntry)
633 ... schema = IAuthorEntry647 ... schema = IAuthorEntry
634648
635 >>> verifyObject(IAuthorEntry, AuthorEntry(A1))649 >>> request = FakeRequest()
650 >>> verifyObject(IAuthorEntry, AuthorEntry(A1, request))
636 True651 True
637652
638The ``schema`` attribute points to the interface class that defines the653The ``schema`` attribute points to the interface class that defines the
@@ -643,18 +658,17 @@
643the interface defined in the schema attribute. This is usually not a problem,658the interface defined in the schema attribute. This is usually not a problem,
644since the schema is usually the interface itself.659since the schema is usually the interface itself.
645660
646 >>> IAuthorEntry.validateInvariants(AuthorEntry(A1))661 >>> IAuthorEntry.validateInvariants(AuthorEntry(A1, request))
647662
648But the invariant will complain if that isn't true.663But the invariant will complain if that isn't true.
649664
650 >>> class InvalidAuthorEntry(Entry):665 >>> class InvalidAuthorEntry(Entry):
651 ... adapts(IAuthor)
652 ... delegates(IAuthorEntry)666 ... delegates(IAuthorEntry)
653 ... schema = ICookbookEntry667 ... schema = ICookbookEntry
654668
655 >>> verifyObject(IAuthorEntry, InvalidAuthorEntry(A1))669 >>> verifyObject(IAuthorEntry, InvalidAuthorEntry(A1, request))
656 True670 True
657 >>> IAuthorEntry.validateInvariants(InvalidAuthorEntry(A1))671 >>> IAuthorEntry.validateInvariants(InvalidAuthorEntry(A1, request))
658 Traceback (most recent call last):672 Traceback (most recent call last):
659 ...673 ...
660 Invalid: InvalidAuthorEntry doesn't provide its ICookbookEntry schema.674 Invalid: InvalidAuthorEntry doesn't provide its ICookbookEntry schema.
@@ -663,40 +677,43 @@
663677
664 >>> class CookbookEntry(Entry):678 >>> class CookbookEntry(Entry):
665 ... """A cookbook, as exposed through the web service."""679 ... """A cookbook, as exposed through the web service."""
666 ... adapts(ICookbook)
667 ... delegates(ICookbookEntry)680 ... delegates(ICookbookEntry)
668 ... schema = ICookbookEntry681 ... schema = ICookbookEntry
669682
670 >>> class DishEntry(Entry):683 >>> class DishEntry(Entry):
671 ... """A dish, as exposed through the web service."""684 ... """A dish, as exposed through the web service."""
672 ... adapts(IDish)
673 ... delegates(IDishEntry)685 ... delegates(IDishEntry)
674 ... schema = IDishEntry686 ... schema = IDishEntry
675687
676 >>> class CommentEntry(Entry):688 >>> class CommentEntry(Entry):
677 ... """A comment, as exposed through the web service."""689 ... """A comment, as exposed through the web service."""
678 ... adapts(IComment)
679 ... delegates(ICommentEntry)690 ... delegates(ICommentEntry)
680 ... schema = ICommentEntry691 ... schema = ICommentEntry
681692
682 >>> class RecipeEntry(Entry):693 >>> class RecipeEntry(Entry):
683 ... adapts(IRecipe)
684 ... delegates(IRecipeEntry)694 ... delegates(IRecipeEntry)
685 ... schema = IRecipeEntry695 ... schema = IRecipeEntry
686696
687We need to register these entries as an adapter from (e.g.) ``IAuthor`` to697We need to register these entries as a multiadapter adapter from
688(e.g.) ``IAuthorEntry``. In ZCML a registration would look like this.698(e.g.) ``IAuthor`` and ``IWebServiceClientRequest`` to (e.g.)
699``IAuthorEntry``. In ZCML a registration would look like this.
689700
690 <adapter factory="my.app.rest.AuthorEntry" />701 <adapter for="my.app.rest.IAuthor
702 lazr.restful.interfaces.IWebServiceClientRequest"
703 factory="my.app.rest.AuthorEntry" />
691704
692Since we're in the middle of a Python example we can do the equivalent705Since we're in the middle of a Python example we can do the equivalent
693in Python code:706in Python code for each entry class:
694707
695 >>> sm.registerAdapter(AuthorEntry, provided=IAuthorEntry)708 >>> for entry_class, adapts_interface, provided_interface in [
696 >>> sm.registerAdapter(CookbookEntry, provided=ICookbookEntry)709 ... [AuthorEntry, IAuthor, IAuthorEntry],
697 >>> sm.registerAdapter(DishEntry, provided=IDishEntry)710 ... [CookbookEntry, ICookbook, ICookbookEntry],
698 >>> sm.registerAdapter(CommentEntry, provided=ICommentEntry)711 ... [DishEntry, IDish, IDishEntry],
699 >>> sm.registerAdapter(RecipeEntry, provided=IRecipeEntry)712 ... [CommentEntry, IComment, ICommentEntry],
713 ... [RecipeEntry, IRecipe, IRecipeEntry]]:
714 ... sm.registerAdapter(
715 ... entry_class, [adapts_interface, IWebServiceClientRequest],
716 ... provided=provided_interface)
700717
701lazr.restful also defines an interface and a base class for collections of718lazr.restful also defines an interface and a base class for collections of
702objects. I'll use it to expose the ``AuthorSet`` collection and other719objects. I'll use it to expose the ``AuthorSet`` collection and other
@@ -708,7 +725,6 @@
708725
709 >>> class AuthorCollection(Collection):726 >>> class AuthorCollection(Collection):
710 ... """A collection of authors, as exposed through the web service."""727 ... """A collection of authors, as exposed through the web service."""
711 ... adapts(IAuthorSet)
712 ...728 ...
713 ... entry_schema = IAuthorEntry729 ... entry_schema = IAuthorEntry
714 ...730 ...
@@ -716,9 +732,11 @@
716 ... """Find all the authors."""732 ... """Find all the authors."""
717 ... return self.context.getAllAuthors()733 ... return self.context.getAllAuthors()
718734
719 >>> sm.registerAdapter(AuthorCollection)735 >>> sm.registerAdapter(AuthorCollection,
736 ... (IAuthorSet, IWebServiceClientRequest),
737 ... provided=ICollection)
720738
721 >>> verifyObject(ICollection, AuthorCollection(AuthorSet()))739 >>> verifyObject(ICollection, AuthorCollection(AuthorSet(), request))
722 True740 True
723741
724 >>> class CookbookCollection(Collection):742 >>> class CookbookCollection(Collection):
@@ -731,7 +749,9 @@
731 ... def find(self):749 ... def find(self):
732 ... """Find all the cookbooks."""750 ... """Find all the cookbooks."""
733 ... return self.context.getAll()751 ... return self.context.getAll()
734 >>> sm.registerAdapter(CookbookCollection)752 >>> sm.registerAdapter(CookbookCollection,
753 ... (ICookbookSet, IWebServiceClientRequest),
754 ... provided=ICollection)
735755
736 >>> class DishCollection(Collection):756 >>> class DishCollection(Collection):
737 ... """A collection of dishes, as exposed through the web service."""757 ... """A collection of dishes, as exposed through the web service."""
@@ -742,7 +762,10 @@
742 ... def find(self):762 ... def find(self):
743 ... """Find all the dishes."""763 ... """Find all the dishes."""
744 ... return self.context.getAll()764 ... return self.context.getAll()
745 >>> sm.registerAdapter(DishCollection)765
766 >>> sm.registerAdapter(DishCollection,
767 ... (IDishSet, IWebServiceClientRequest),
768 ... provided=ICollection)
746769
747Like ``Entry``, ``Collection`` is a simple base class that defines a770Like ``Entry``, ``Collection`` is a simple base class that defines a
748constructor. The ``entry_schema`` attribute gives a ``Collection`` class771constructor. The ``entry_schema`` attribute gives a ``Collection`` class
@@ -762,8 +785,9 @@
762785
763 >>> def scope_collection(parent, child, name):786 >>> def scope_collection(parent, child, name):
764 ... """A helper method that simulates a scoped collection lookup."""787 ... """A helper method that simulates a scoped collection lookup."""
765 ... parent_entry = IEntry(parent)788 ... parent_entry = getMultiAdapter((parent, request), IEntry)
766 ... scoped = getMultiAdapter((parent_entry, IEntry(child)),789 ... child_entry = getMultiAdapter((child, request), IEntry)
790 ... scoped = getMultiAdapter((parent_entry, child_entry, request),
767 ... IScopedCollection)791 ... IScopedCollection)
768 ... scoped.relationship = parent_entry.schema.get(name)792 ... scoped.relationship = parent_entry.schema.get(name)
769 ... return scoped793 ... return scoped
770794
=== modified file 'src/lazr/restful/example/base/root.py'
--- src/lazr/restful/example/base/root.py 2009-11-12 19:08:10 +0000
+++ src/lazr/restful/example/base/root.py 2010-01-12 15:21:22 +0000
@@ -16,12 +16,11 @@
1616
17from zope.interface import implements17from zope.interface import implements
18from zope.location.interfaces import ILocation18from zope.location.interfaces import ILocation
19from zope.component import adapts, getUtility19from zope.component import adapts, getMultiAdapter, getUtility
20from zope.schema.interfaces import IBytes20from zope.schema.interfaces import IBytes
2121
22from lazr.restful import ServiceRootResource22from lazr.restful import directives, ServiceRootResource
2323
24from lazr.restful import directives
25from lazr.restful.interfaces import (24from lazr.restful.interfaces import (
26 IByteStorage, IEntry, IServiceRootResource, ITopLevelEntryLink,25 IByteStorage, IEntry, IServiceRootResource, ITopLevelEntryLink,
27 IWebServiceConfiguration)26 IWebServiceConfiguration)
@@ -30,6 +29,7 @@
30 IFileManager, IRecipe, IRecipeSet, IHasGet, NameAlreadyTaken)29 IFileManager, IRecipe, IRecipeSet, IHasGet, NameAlreadyTaken)
31from lazr.restful.simple import BaseWebServiceConfiguration30from lazr.restful.simple import BaseWebServiceConfiguration
32from lazr.restful.testing.webservice import WebServiceTestPublication31from lazr.restful.testing.webservice import WebServiceTestPublication
32from lazr.restful.utils import get_current_browser_request
3333
3434
35#Entry classes.35#Entry classes.
@@ -148,7 +148,8 @@
148 self.recipes.remove(recipe)148 self.recipes.remove(recipe)
149149
150 def replace_cover(self, cover):150 def replace_cover(self, cover):
151 storage = SimpleByteStorage(IEntry(self), ICookbook['cover'])151 entry = getMultiAdapter((self, get_current_browser_request()), IEntry)
152 storage = SimpleByteStorage(entry, ICookbook['cover'])
152 storage.createStored('application/octet-stream', cover, 'cover')153 storage.createStored('application/octet-stream', cover, 'cover')
153154
154155
155156
=== modified file 'src/lazr/restful/interfaces/_rest.py'
--- src/lazr/restful/interfaces/_rest.py 2009-11-18 17:33:20 +0000
+++ src/lazr/restful/interfaces/_rest.py 2010-01-12 15:21:22 +0000
@@ -49,6 +49,7 @@
49 'LAZR_WEBSERVICE_NS',49 'LAZR_WEBSERVICE_NS',
50 'IWebServiceClientRequest',50 'IWebServiceClientRequest',
51 'IWebServiceLayer',51 'IWebServiceLayer',
52 'IWebServiceVersion',
52 ]53 ]
5354
54from zope.schema import Bool, Int, List, TextLine55from zope.schema import Bool, Int, List, TextLine
@@ -251,13 +252,27 @@
251252
252253
253class IWebServiceClientRequest(IBrowserRequest):254class IWebServiceClientRequest(IBrowserRequest):
254 """Marker interface requests to the web service."""255 """Interface for requests to the web service."""
256 version = Attribute("The version of the web service that the client "
257 "requested.")
255258
256259
257class IWebServiceLayer(IWebServiceClientRequest, IDefaultBrowserLayer):260class IWebServiceLayer(IWebServiceClientRequest, IDefaultBrowserLayer):
258 """Marker interface for registering views on the web service."""261 """Marker interface for registering views on the web service."""
259262
260263
264class IWebServiceVersion(Interface):
265 """Used to register IWebServiceClientRequest subclasses as utilities.
266
267 Every version of a web service must register a subclass of
268 IWebServiceClientRequest as an IWebServiceVersion utility, with a
269 name that's the web service version name. For instance:
270
271 registerUtility(IWebServiceClientRequestBeta,
272 IWebServiceVersion, name="beta")
273 """
274 pass
275
261class IJSONRequestCache(Interface):276class IJSONRequestCache(Interface):
262 """A cache of objects exposed as URLs or JSON representations."""277 """A cache of objects exposed as URLs or JSON representations."""
263278
264279
=== modified file 'src/lazr/restful/metazcml.py'
--- src/lazr/restful/metazcml.py 2009-04-16 20:45:55 +0000
+++ src/lazr/restful/metazcml.py 2010-01-12 15:21:22 +0000
@@ -82,8 +82,14 @@
82 context.action(82 context.action(
83 discriminator=('adapter', interface, provides, ''),83 discriminator=('adapter', interface, provides, ''),
84 callable=handler,84 callable=handler,
85 # XXX leonardr bug=503948 Register the adapter against a
86 # generic IWebServiceClientRequest. It will be picked up
87 # for all versions of the web service. Later on, this will
88 # be changed to register different adapters for different
89 # versions.
85 args=('registerAdapter',90 args=('registerAdapter',
86 factory, (interface, ), provides, '', context.info),91 factory, (interface, IWebServiceClientRequest),
92 provides, '', context.info),
87 )93 )
88 register_webservice_operations(context, interface)94 register_webservice_operations(context, interface)
8995
9096
=== modified file 'src/lazr/restful/publisher.py'
--- src/lazr/restful/publisher.py 2009-11-19 15:53:26 +0000
+++ src/lazr/restful/publisher.py 2010-01-12 15:21:22 +0000
@@ -35,7 +35,7 @@
35from lazr.restful.interfaces import (35from lazr.restful.interfaces import (
36 IByteStorage, ICollection, ICollectionField, IEntry, IEntryField,36 IByteStorage, ICollection, ICollectionField, IEntry, IEntryField,
37 IHTTPResource, IServiceRootResource, IWebBrowserInitiatedRequest,37 IHTTPResource, IServiceRootResource, IWebBrowserInitiatedRequest,
38 IWebServiceClientRequest, IWebServiceConfiguration)38 IWebServiceClientRequest, IWebServiceConfiguration, IWebServiceVersion)
3939
4040
41class WebServicePublicationMixin:41class WebServicePublicationMixin:
@@ -57,8 +57,10 @@
57 # handle traversing to the scoped collection itself.57 # handle traversing to the scoped collection itself.
58 if len(request.getTraversalStack()) == 0:58 if len(request.getTraversalStack()) == 0:
59 try:59 try:
60 entry = IEntry(ob)60 entry = getMultiAdapter((ob, request), IEntry)
61 except TypeError:61 except ComponentLookupError:
62 # This doesn't look like a lazr.restful object. Let
63 # the superclass handle traversal.
62 pass64 pass
63 else:65 else:
64 if name.endswith("_link"):66 if name.endswith("_link"):
@@ -111,7 +113,7 @@
111 collection = getattr(entry, name, None)113 collection = getattr(entry, name, None)
112 if collection is None:114 if collection is None:
113 return None115 return None
114 scoped_collection = ScopedCollection(entry.context, entry)116 scoped_collection = ScopedCollection(entry.context, entry, request)
115 # Tell the IScopedCollection object what collection it's managing,117 # Tell the IScopedCollection object what collection it's managing,
116 # and what the collection's relationship is to the entry it's118 # and what the collection's relationship is to the entry it's
117 # scoped to.119 # scoped to.
@@ -138,11 +140,11 @@
138 appropriate resource.140 appropriate resource.
139 """141 """
140 if (ICollection.providedBy(ob) or142 if (ICollection.providedBy(ob) or
141 queryAdapter(ob, ICollection) is not None):143 queryMultiAdapter((ob, request), ICollection) is not None):
142 # Object supports ICollection protocol.144 # Object supports ICollection protocol.
143 resource = CollectionResource(ob, request)145 resource = CollectionResource(ob, request)
144 elif (IEntry.providedBy(ob) or146 elif (IEntry.providedBy(ob) or
145 queryAdapter(ob, IEntry) is not None):147 queryMultiAdapter((ob, request), IEntry) is not None):
146 # Object supports IEntry protocol.148 # Object supports IEntry protocol.
147 resource = EntryResource(ob, request)149 resource = EntryResource(ob, request)
148 elif (IEntryField.providedBy(ob) or150 elif (IEntryField.providedBy(ob) or
@@ -255,11 +257,17 @@
255 raise NotFound(self, '', self)257 raise NotFound(self, '', self)
256 self.annotations[self.VERSION_ANNOTATION] = version258 self.annotations[self.VERSION_ANNOTATION] = version
257259
260 # Find the version-specific interface this request should
261 # provide, and provide it.
262 to_provide = getUtility(IWebServiceVersion, name=version)
263 alsoProvides(self, to_provide)
264 self.version = version
265
258 # Find the appropriate service root for this version and set266 # Find the appropriate service root for this version and set
259 # the publication's application appropriately.267 # the publication's application appropriately.
260 try:268 try:
261 # First, try to find a version-specific service root.269 # First, try to find a version-specific service root.
262 service_root = getUtility(IServiceRootResource, name=version)270 service_root = getUtility(IServiceRootResource, name=self.version)
263 except ComponentLookupError:271 except ComponentLookupError:
264 # Next, try a version-independent service root.272 # Next, try a version-independent service root.
265 service_root = getUtility(IServiceRootResource)273 service_root = getUtility(IServiceRootResource)
266274
=== modified file 'src/lazr/restful/simple.py'
--- src/lazr/restful/simple.py 2009-11-12 17:03:06 +0000
+++ src/lazr/restful/simple.py 2010-01-12 15:21:22 +0000
@@ -22,7 +22,9 @@
22from zope.publisher.browser import BrowserRequest22from zope.publisher.browser import BrowserRequest
23from zope.publisher.interfaces import IPublication, IPublishTraverse, NotFound23from zope.publisher.interfaces import IPublication, IPublishTraverse, NotFound
24from zope.publisher.publish import mapply24from zope.publisher.publish import mapply
25from zope.proxy import sameProxiedObjects
25from zope.security.management import endInteraction, newInteraction26from zope.security.management import endInteraction, newInteraction
27from zope.traversing.browser import AbsoluteURL as ZopeAbsoluteURL
26from zope.traversing.browser.interfaces import IAbsoluteURL28from zope.traversing.browser.interfaces import IAbsoluteURL
27from zope.traversing.browser.absoluteurl import _insufficientContext, _safe29from zope.traversing.browser.absoluteurl import _insufficientContext, _safe
2830
@@ -188,7 +190,8 @@
188 # First collect the top-level collections.190 # First collect the top-level collections.
189 for name, (schema_interface, obj) in (191 for name, (schema_interface, obj) in (
190 self.top_level_collections.items()):192 self.top_level_collections.items()):
191 adapter = EntryAdapterUtility.forSchemaInterface(schema_interface)193 adapter = EntryAdapterUtility.forSchemaInterface(
194 schema_interface, self.request)
192 link_name = ("%s_collection_link" % adapter.plural_type)195 link_name = ("%s_collection_link" % adapter.plural_type)
193 top_level_resources[link_name] = obj196 top_level_resources[link_name] = obj
194 # Then collect the top-level entries.197 # Then collect the top-level entries.
195198
=== modified file 'src/lazr/restful/tales.py'
--- src/lazr/restful/tales.py 2009-05-04 19:11:00 +0000
+++ src/lazr/restful/tales.py 2010-01-12 15:21:22 +0000
@@ -13,7 +13,8 @@
13from epydoc.markup import DocstringLinker13from epydoc.markup import DocstringLinker
14from epydoc.markup.restructuredtext import parse_docstring14from epydoc.markup.restructuredtext import parse_docstring
1515
16from zope.component import adapts, queryAdapter, getGlobalSiteManager16from zope.component import (
17 adapts, getGlobalSiteManager, getUtility, queryMultiAdapter)
17from zope.interface import implements18from zope.interface import implements
18from zope.interface.interfaces import IInterface19from zope.interface.interfaces import IInterface
19from zope.schema import getFieldsInOrder20from zope.schema import getFieldsInOrder
@@ -31,7 +32,8 @@
31 ICollection, ICollectionField, IEntry, IJSONRequestCache,32 ICollection, ICollectionField, IEntry, IJSONRequestCache,
32 IReferenceChoice, IResourceDELETEOperation, IResourceGETOperation,33 IReferenceChoice, IResourceDELETEOperation, IResourceGETOperation,
33 IResourceOperation, IResourcePOSTOperation, IScopedCollection,34 IResourceOperation, IResourcePOSTOperation, IScopedCollection,
34 ITopLevelEntryLink, IWebServiceClientRequest, LAZR_WEBSERVICE_NAME)35 ITopLevelEntryLink, IWebServiceClientRequest, IWebServiceVersion,
36 LAZR_WEBSERVICE_NAME)
35from lazr.restful.utils import get_current_browser_request37from lazr.restful.utils import get_current_browser_request
3638
3739
@@ -108,13 +110,14 @@
108 @property110 @property
109 def is_entry(self):111 def is_entry(self):
110 """Whether the object is published as an entry."""112 """Whether the object is published as an entry."""
111 return queryAdapter(self.context, IEntry) != None113 return queryMultiAdapter(
114 (self.context, get_current_browser_request()), IEntry) != None
112115
113 @property116 @property
114 def json(self):117 def json(self):
115 """Return a JSON description of the object."""118 """Return a JSON description of the object."""
116 request = IWebServiceClientRequest(get_current_browser_request())119 request = IWebServiceClientRequest(get_current_browser_request())
117 if queryAdapter(self.context, IEntry):120 if queryMultiAdapter((self.context, request), IEntry):
118 resource = EntryResource(self.context, request)121 resource = EntryResource(self.context, request)
119 else:122 else:
120 # Just dump it as JSON.123 # Just dump it as JSON.
@@ -283,9 +286,11 @@
283 # adapting.286 # adapting.
284 model_class = self._model_class287 model_class = self._model_class
285 operations = []288 operations = []
289 request_interface = getUtility(
290 IWebServiceVersion, get_current_browser_request().version)
286 for interface in (IResourceGETOperation, IResourcePOSTOperation):291 for interface in (IResourceGETOperation, IResourcePOSTOperation):
287 operations.extend(getGlobalSiteManager().adapters.lookupAll(292 operations.extend(getGlobalSiteManager().adapters.lookupAll(
288 (model_class, IWebServiceClientRequest), interface))293 (model_class, request_interface), interface))
289 return [{'name' : name, 'op' : op} for name, op in operations]294 return [{'name' : name, 'op' : op} for name, op in operations]
290295
291296
@@ -297,7 +302,8 @@
297 def __init__(self, entry_interface):302 def __init__(self, entry_interface):
298 super(WadlEntryInterfaceAdapterAPI, self).__init__(303 super(WadlEntryInterfaceAdapterAPI, self).__init__(
299 entry_interface, IEntry)304 entry_interface, IEntry)
300 self.utility = EntryAdapterUtility.forEntryInterface(entry_interface)305 self.utility = EntryAdapterUtility.forEntryInterface(
306 entry_interface, get_current_browser_request())
301307
302 @property308 @property
303 def entry_page_representation_link(self):309 def entry_page_representation_link(self):
@@ -370,8 +376,10 @@
370 @property376 @property
371 def supports_delete(self):377 def supports_delete(self):
372 """Return true if this entry responds to DELETE."""378 """Return true if this entry responds to DELETE."""
379 request_interface = getUtility(
380 IWebServiceVersion, get_current_browser_request().version)
373 operations = getGlobalSiteManager().adapters.lookupAll(381 operations = getGlobalSiteManager().adapters.lookupAll(
374 (self._model_class, IWebServiceClientRequest),382 (self._model_class, request_interface),
375 IResourceDELETEOperation)383 IResourceDELETEOperation)
376 return len(operations) > 0384 return len(operations) > 0
377385
@@ -506,7 +514,8 @@
506 raise TypeError("Field is not of a supported type.")514 raise TypeError("Field is not of a supported type.")
507 assert schema is not IObject, (515 assert schema is not IObject, (
508 "Null schema provided for %s" % self.field.__name__)516 "Null schema provided for %s" % self.field.__name__)
509 return EntryAdapterUtility.forSchemaInterface(schema)517 return EntryAdapterUtility.forSchemaInterface(
518 schema, get_current_browser_request())
510519
511520
512 @property521 @property
@@ -530,7 +539,8 @@
530539
531 def type_link(self):540 def type_link(self):
532 return EntryAdapterUtility.forSchemaInterface(541 return EntryAdapterUtility.forSchemaInterface(
533 self.entry_link.entry_type).type_link542 self.entry_link.entry_type,
543 get_current_browser_request()).type_link
534544
535545
536class WadlOperationAPI(RESTUtilityBase):546class WadlOperationAPI(RESTUtilityBase):
537547
=== modified file 'src/lazr/restful/testing/webservice.py'
--- src/lazr/restful/testing/webservice.py 2009-11-19 16:28:38 +0000
+++ src/lazr/restful/testing/webservice.py 2010-01-12 15:21:22 +0000
@@ -104,7 +104,11 @@
104 # get_current_browser_request()104 # get_current_browser_request()
105 implements(IHTTPApplicationRequest, IWebServiceLayer)105 implements(IHTTPApplicationRequest, IWebServiceLayer)
106106
107 def __init__(self, traversed=None, stack=None):107 def __init__(self, traversed=None, stack=None, version=None):
108 if version is None:
109 config = getUtility(IWebServiceConfiguration)
110 version = config.latest_version_uri_prefix
111 self.version = version
108 self._traversed_names = traversed112 self._traversed_names = traversed
109 self._stack = stack113 self._stack = stack
110 self.response = FakeResponse()114 self.response = FakeResponse()
111115
=== modified file 'src/lazr/restful/tests/test_navigation.py'
--- src/lazr/restful/tests/test_navigation.py 2009-07-27 02:27:38 +0000
+++ src/lazr/restful/tests/test_navigation.py 2010-01-12 15:21:22 +0000
@@ -6,80 +6,101 @@
66
7import unittest7import unittest
88
9from zope.component import getSiteManager
9from zope.interface import Interface, implements10from zope.interface import Interface, implements
10from zope.publisher.interfaces import NotFound11from zope.publisher.interfaces import NotFound
11from zope.schema import Text, Object12from zope.schema import Text, Object
13from zope.testing.cleanup import cleanUp
1214
13from lazr.restful.interfaces import IEntry15from lazr.restful.interfaces import IEntry, IWebServiceClientRequest
14from lazr.restful.publisher import WebServicePublicationMixin16from lazr.restful.simple import Publication
17from lazr.restful.testing.webservice import FakeRequest
1518
1619
17class IChild(Interface):20class IChild(Interface):
21 """Interface for a simple entry."""
18 one = Text(title=u'One')22 one = Text(title=u'One')
19 two = Text(title=u'Two')23 two = Text(title=u'Two')
2024
2125
22class IChildEntry(IChild, IEntry):
23 pass
24
25
26class IParent(Interface):26class IParent(Interface):
27 """Interface for a simple entry that contains another entry."""
27 three = Text(title=u'Three')28 three = Text(title=u'Three')
28 child = Object(schema=IChild)29 child = Object(schema=IChild)
2930
3031
31class IParentEntry(IParent, IEntry):
32 pass
33
34
35class Child:32class Child:
36 implements(IChildEntry)33 """A simple implementation of IChild."""
37 schema = IChild34 implements(IChild)
38
39 one = u'one'35 one = u'one'
40 two = u'two'36 two = u'two'
4137
4238
39class ChildEntry:
40 """Implementation of an entry wrapping a Child."""
41 schema = IChild
42 def __init__(self, context, request):
43 self.context = context
44
45
43class Parent:46class Parent:
44 implements(IParentEntry)47 """A simple implementation of IParent."""
45 schema = IParent48 implements(IParent)
46
47 three = u'three'49 three = u'three'
48 child = Child()50 child = Child()
4951
52
53class ParentEntry:
54 """Implementation of an entry wrapping a Parent, containing a Child."""
55 schema = IParent
56
57 def __init__(self, context, request):
58 self.context = context
59
50 @property60 @property
51 def context(self):61 def child(self):
52 return self62 return self.context.child
5363
5464
55class FakeRequest:65class FakeRequestWithEmptyTraversalStack(FakeRequest):
56 """A fake request satisfying `traverseName()`."""66 """A fake request satisfying `traverseName()`."""
5767
58 def getTraversalStack(self):68 def getTraversalStack(self):
59 return ()69 return ()
6070
6171
62class NavigationPublication(WebServicePublicationMixin):
63 pass
64
65
66class NavigationTestCase(unittest.TestCase):72class NavigationTestCase(unittest.TestCase):
6773
74 def setUp(self):
75 # Register ChildEntry as the IEntry implementation for IChild.
76 sm = getSiteManager()
77 sm.registerAdapter(
78 ChildEntry, [IChild, IWebServiceClientRequest], provided=IEntry)
79
80 # Register ParentEntry as the IEntry implementation for IParent.
81 sm.registerAdapter(
82 ParentEntry, [IParent, IWebServiceClientRequest], provided=IEntry)
83
84 def tearDown(self):
85 cleanUp()
86
68 def test_toplevel_navigation(self):87 def test_toplevel_navigation(self):
69 # Test that publication can reach sub-entries.88 # Test that publication can reach sub-entries.
70 publication = NavigationPublication()89 publication = Publication(None)
71 obj = publication.traverseName(FakeRequest(), Parent(), 'child')90 request = FakeRequestWithEmptyTraversalStack(version='trunk')
91 obj = publication.traverseName(request, Parent(), 'child')
72 self.assertEqual(obj.one, 'one')92 self.assertEqual(obj.one, 'one')
7393
74 def test_toplevel_navigation_without_subentry(self):94 def test_toplevel_navigation_without_subentry(self):
75 # Test that publication raises NotFound when subentry attribute95 # Test that publication raises NotFound when subentry attribute
76 # returns None.96 # returns None.
97 request = FakeRequestWithEmptyTraversalStack(version='trunk')
77 parent = Parent()98 parent = Parent()
78 parent.child = None99 parent.child = None
79 publication = NavigationPublication()100 publication = Publication(None)
80 self.assertRaises(101 self.assertRaises(
81 NotFound, publication.traverseName,102 NotFound, publication.traverseName,
82 FakeRequest(), parent, 'child')103 request, parent, 'child')
83104
84105
85def additional_tests():106def additional_tests():
86107
=== modified file 'src/lazr/restful/tests/test_webservice.py'
--- src/lazr/restful/tests/test_webservice.py 2009-08-11 18:36:20 +0000
+++ src/lazr/restful/tests/test_webservice.py 2010-01-12 15:21:22 +0000
@@ -9,22 +9,26 @@
9from types import ModuleType9from types import ModuleType
10import unittest10import unittest
1111
12from zope.component import getGlobalSiteManager12from zope.component import getGlobalSiteManager, getUtility
13from zope.configuration import xmlconfig13from zope.configuration import xmlconfig
14from zope.interface import implements, Interface14from zope.interface import alsoProvides, implements, Interface
15from zope.schema import Date, Datetime, TextLine15from zope.schema import Date, Datetime, TextLine
16from zope.testing.cleanup import CleanUp16from zope.testing.cleanup import CleanUp
1717
18from lazr.restful.fields import Reference18from lazr.restful.fields import Reference
19from lazr.restful.interfaces import (19from lazr.restful.interfaces import (
20 ICollection, IEntry, IEntryResource, IResourceGETOperation,20 ICollection, IEntry, IEntryResource, IResourceGETOperation,
21 IWebServiceClientRequest)21 IWebServiceClientRequest, IWebServiceVersion)
22from lazr.restful import EntryResource, ServiceRootResource, ResourceGETOperation22from lazr.restful import EntryResource, ServiceRootResource, ResourceGETOperation
23from lazr.restful.simple import Request23from lazr.restful.simple import BaseWebServiceConfiguration, Request
24from lazr.restful.declarations import (24from lazr.restful.declarations import (
25 collection_default_content, exported, export_as_webservice_collection,25 collection_default_content, exported, export_as_webservice_collection,
26 export_as_webservice_entry, export_read_operation, operation_parameters)26 export_as_webservice_entry, export_read_operation, operation_parameters)
27from lazr.restful.interfaces import IWebServiceConfiguration
28from lazr.restful.testing.webservice import (
29 create_web_service_request, WebServiceTestPublication)
27from lazr.restful.testing.tales import test_tales30from lazr.restful.testing.tales import test_tales
31from lazr.restful.utils import get_current_browser_request
2832
2933
30def get_resource_factory(model_interface, resource_interface):34def get_resource_factory(model_interface, resource_interface):
@@ -36,8 +40,9 @@
36 `IEntry` or `ICollection`.40 `IEntry` or `ICollection`.
37 :return: the resource factory (the autogenerated adapter class.41 :return: the resource factory (the autogenerated adapter class.
38 """42 """
39 return getGlobalSiteManager().adapters.lookup1(43 request_interface = getUtility(IWebServiceVersion, name='trunk')
40 model_interface, resource_interface)44 return getGlobalSiteManager().adapters.lookup(
45 (model_interface, request_interface), resource_interface)
4146
4247
43def get_operation_factory(model_interface, name):48def get_operation_factory(model_interface, name):
@@ -87,6 +92,23 @@
87 """Returns all the entries."""92 """Returns all the entries."""
8893
8994
95class SimpleWebServiceConfiguration(BaseWebServiceConfiguration):
96 implements(IWebServiceConfiguration)
97 show_tracebacks = False
98 latest_version_uri_prefix = 'trunk'
99 hostname = "webservice_test"
100
101 def createRequest(self, body_instream, environ):
102 request = Request(body_instream, environ)
103 request.setPublication(WebServiceTestPublication(None))
104 request.version = 'trunk'
105 return request
106
107
108class IWebServiceRequestTrunk(IWebServiceClientRequest):
109 """A marker interface for requests to the 'trunk' web service."""
110
111
90class WebServiceTestCase(CleanUp, unittest.TestCase):112class WebServiceTestCase(CleanUp, unittest.TestCase):
91 """A test case for web service operations."""113 """A test case for web service operations."""
92114
@@ -96,6 +118,17 @@
96 """Set the component registry with the given model."""118 """Set the component registry with the given model."""
97 super(WebServiceTestCase, self).setUp()119 super(WebServiceTestCase, self).setUp()
98120
121 # Register a simple configuration object.
122 webservice_configuration = SimpleWebServiceConfiguration()
123 sm = getGlobalSiteManager()
124 sm.registerUtility(webservice_configuration)
125
126 # Register an IWebServiceVersion for the
127 # 'trunk' web service version.
128 alsoProvides(IWebServiceRequestTrunk, IWebServiceVersion)
129 sm.registerUtility(
130 IWebServiceRequestTrunk, IWebServiceVersion, name='trunk')
131
99 # Build a test module that exposes the given resource interfaces.132 # Build a test module that exposes the given resource interfaces.
100 testmodule = ModuleType('testmodule')133 testmodule = ModuleType('testmodule')
101 for interface in self.testmodule_objects:134 for interface in self.testmodule_objects:
@@ -195,8 +228,8 @@
195 entry_class = get_resource_factory(IHasRestrictedField, IEntry)228 entry_class = get_resource_factory(IHasRestrictedField, IEntry)
196 request = Request(StringIO(""), {})229 request = Request(StringIO(""), {})
197230
198 entry = entry_class(HasRestrictedField(""))231 entry = entry_class(HasRestrictedField(""), request)
199 resource = EntryResource(entry, request)232 resource = EntryResource(HasRestrictedField(""), request)
200233
201 entry.schema['a_field'].restrict_to_interface = IHasRestrictedField234 entry.schema['a_field'].restrict_to_interface = IHasRestrictedField
202 resource.applyChanges({'a_field': 'a_value'})235 resource.applyChanges({'a_field': 'a_value'})
@@ -308,6 +341,7 @@
308 This will fail due to a name conflict.341 This will fail due to a name conflict.
309 """342 """
310 resource = ServiceRootResource()343 resource = ServiceRootResource()
344 request = create_web_service_request('/')
311 try:345 try:
312 resource.toWADL()346 resource.toWADL()
313 self.fail('Expected toWADL to fail with an AssertionError')347 self.fail('Expected toWADL to fail with an AssertionError')
314348
=== modified file 'src/lazr/restful/utils.py'
--- src/lazr/restful/utils.py 2009-10-12 20:47:21 +0000
+++ src/lazr/restful/utils.py 2010-01-12 15:21:22 +0000
@@ -7,6 +7,7 @@
7 'camelcase_to_underscore_separated',7 'camelcase_to_underscore_separated',
8 'get_current_browser_request',8 'get_current_browser_request',
9 'implement_from_dict',9 'implement_from_dict',
10 'make_identifier_safe',
10 'safe_js_escape',11 'safe_js_escape',
11 'safe_hasattr',12 'safe_hasattr',
12 'smartquote',13 'smartquote',
@@ -16,6 +17,7 @@
1617
17import cgi18import cgi
18import re19import re
20import string
19import subprocess21import subprocess
2022
21from simplejson import encoder23from simplejson import encoder
@@ -51,6 +53,21 @@
51 return new_class53 return new_class
5254
5355
56def make_identifier_safe(name):
57 """Change a string so it can be used as a Python identifier.
58
59 Changes all characters other than letters, numbers, and underscore
60 into underscore. If the first character is not a letter or
61 underscore, prepends an underscore.
62 """
63 if name is None:
64 raise ValueError("Cannot make None value identifier-safe.")
65 name = re.sub("[^A-Za-z0-9_]", "_", name)
66 if len(name) == 0 or name[0] not in string.letters and name[0] != '_':
67 name = '_' + name
68 return name
69
70
54def camelcase_to_underscore_separated(name):71def camelcase_to_underscore_separated(name):
55 """Convert 'ACamelCaseString' to 'a_camel_case_string'"""72 """Convert 'ACamelCaseString' to 'a_camel_case_string'"""
56 def prepend_underscore(match):73 def prepend_underscore(match):

Subscribers

People subscribed via source and target branches