Publication Post-Processing
Note that this is a brainstorming document, not a proposal.
Problem
Informal Publishing API
The original Zope publisher was designed to make writing simple CGI programs fairly transparent:
- Translate a URL path to a series of object-access operations, starting from a root object that was, originally, a published module.
- Call the object resulting from path traversal. Perform method/function introspection to determine argument signatures and pass arguments obtained from request meta data, such as form data, cookies, headers, etc.
- Convert the result of the call to a string and construct an HTTP response from it.
Over time, as applications became more complex, various hooks were added to provide control over root objects and traversal rules, and to support transactions and security. In Zope 3, these hooks were formalized in the publication APIs?.
Two major parts of the publishing process remain mostly informal:
- Calling an object, and
- Converting the call result to an HTTP Response.
Zope 3 (like Zope 2) lacks any formal API defining how objects, found by way of path traversal, should be published. This is evidenced by the fact that views that provide pages are registered as providing "Interface". There is an informal API, which is that object will be called with arguments determined through method introspection.
It is up to response objects to interpret call results. Recently, HTTP responses defined an HTTP result API that defines the last step in the publishing process.
Need for page-composition support
Traditionally, in Zope, pages are computed in a top-down fashion, using templates such as ZPT or DTML templates. This approach has an unfortunate impact of mixing concerns of the designer of a site look at feel and the concerns of the person writing an application. The developer of a template is responsible for invoking special templates or APIs? that provide look at feel. This is very brittle and burdensome for both the individual page designer and the people responsible for site look and feel.
In addition to look at feel concerns, it might be desirable to post-process pages in other ways. For example, pages might use Javascript functions or CSS styles that aren't defined by the pages. Pages might be defined this way because they are assembled from pieces, many of which share the same functions or styles. We might not want to define these every time a part that needs them is used. Rather we might want to note the need and then assemble the needed parts as a separate step in page composition.
Technologies
Pipelines
Pipelines are a common model for page composition. Data are passed through a series of steps. Each step transforms the data, producing new data. At the end of the pipeline is data suitable for use as a result. The work of creating a page is spread over the steps in the pipeline.
Pipelines can be static or dynamic. A static pipeline is a fixed sequence of steps. A dynamic pipeline uses rules to determine the sequence of steps. Cocoon is an example of a system with dynamic pipelines.
An advantage of pipelines is that segments are ordered. This allows a single complex computation to be broken down into interdependent pieces. A disadvantage of static pipelines is that the segments are ordered. :) Someone must decide how segments are strung together.
Static pipelines are easy enough to assemble manually in Python. Dynamic pipelines require some sort of mechanism to perform assembly based on rules.
Transitive adaptation
Adapters are similar to pipeline segments. They operate on one or more inputs and produce an output, which can be fed to additional adapters. With Python's iteration protocols, adapters become even more like traditional pipeline segments, by operating on streams of data. Stringing adapters together can have the same effect as assembling a pipeline. Because adapters are chosen dynamically, the adapters chosen for a particular instance if a pipeline can vary depending on the information flowing through the adapters, much as with dynamic pipelines.
Zope 3 doesn't currently provide support for automatic transitive adaptation. It's possible that that transitive adaptation (or something like it might be a basis for a component-based pipeline realization.
Subscription
Subscriptions provides a highly loosely-coupled means of computation. Subscribers react to events independently. Subscribers are unordered and are appropriate for dealing with separate concerns. Consider the 2 use cases mentioned above, providing a site look and feel, and adding needed Javascript or CSS definitions. These tasks are independent and can be applied in any order.
(Note, subscribers, as currently implemented in Zope 3 are, in fact, ordered, although applications are not supposed to depend on the existing ordering. The existing order is:
- More general subscribers, those registered for more general events, are executed before more specific subscribers.
- For equivalent specificity, subscribers follow order of definition.
Many applications do, in fact, depend on this ordering. It might be argued that invoking more general subscribers first is, in fact, a reasonable, as it allows specific subscribers to build on work done by more general subscribers.)
Brainstorming
This section outlines a possible solution. The intent is not to make a proposal, but, rather, to provide a stimulus for discussion.
We define an interface for something that is published. Perhaps this is IPublished?. This interface defines a method that is called to produce a publication result. A default adapter to IPublished? would perform classic Zope method introspection.
We define another interface for a publication result (e.g. IPublicationResult?). The result of calling an IPublished? is adapted to IPublicationResult?.
There is some sort of adaptation process for getting from the result of an IPublished? to an IPublicationResult?. Perhaps this is a traditional simple adaptation or perhaps it's some kind of transitive or iterative adaptation model. Maybe, to get started, it doesn't matter. For example, perhaps there's a default simple adapter whos implementation actually uses a more sophisticated adaptation mechanism. In any case, at each step along the way, we generate some events.
(Note that, in the case of HTTP, the HTTP response will further adapt an IPublicationResult? to an HTTP result.)
Here's a possible implementation of the publication (zope.publisher.interfaces.IPublication?) `callObject` method that implements this sketch:
def callObject(self, request, ob):
if not IPublished.providedBy(ob):
published = component.getMultiAdapter((ob, request), IPublished)
else:
published = ob
notify(Published(published, request))
result = ob.getResult()
if not IPublicationResult.providedBy(result):
result = component.getMultiAdapter((result, request),
IPublicationResult)
notify(PublicationResult(result, request))
return result
(For comparison, the current `callObject` method implementation is:
def callObject(self, request, ob):
return mapply(ob, request.getPositionalArguments(), request)
)
Note that with this change, we would register pages as providing IPublished?.
So how can we use this to address our original use cases?
The simplest approach might be to provide simple subscribers that transform the publication result, assuming that the result is mutable.
