[PEAK] peak.web site configuration
Phillip J. Eby
pje at telecommunity.com
Tue Oct 5 14:55:15 EDT 2004
[As always, your comments and questions are welcome and requested.]
Requirements
============
These requirements mainly address Tier 1 functionality, with a few notes on
higher-tier items that could maybe be done sooner.
High Priority
-------------
* Define locations, beginning with the application's root
* Define views for objects of a particular type or interface, that are
applicable within the current location, along with their required permission(s)
- Define a view's implementation by specifying a resource name
- Define a view's implementation by specifying a Python expression
* Declare the permissions required within the current location to access
attributes of objects of a particular type or interface
* Extensible format: should be easy to add new kinds of registration and
declaration within pretty much arbitrary locations.
* Have a way to specify that a given location will look for subobjects
using certain Python mappings or functions (akin to the Specialist->Rack
mapping in ZPatterns)
* Specify how to compute a canonical absolute URL for objects of a
particular type or interface (so that types can be effectively associated
with a Specialist)
Medium Priority
---------------
* Define permission classes
* Locations and their configuration should be reusable
* Define page sets (ala ZCML browser:page) w/out duplicating specification
* If an XML format is used, align attribute and element names with ZCML if
possible
Low Priority
------------
* Define menus and menu items (this is a Tier 2 or 3 feature)
* Annotate a a view as being a menu item, with a title (ditto)
* Define principals and grant them permissions (not required until Tier 2)
* Define skins, layers, namespaces, etc. (these can already be done in an
.ini, so it's potentially duplicative)
* "View folders" -- define a view with sub-locations of its own, to group
an object's methods into separate URL paths
Designing the Language
======================
Does this really need to be XML, with the associated overhead
thereof? Whatever we implement will have to be Python objects anyway, so
why can't we just use Python objects?
Well, we could, but the syntax would end up fairly weird. Many definitions
need dotted names or names that aren't otherwise usable as Python attribute
names or keyword arguments, for example, so we'd just end up writing a kind
of "pseudo-XML" in Python.
So, for the time being, let's assume it's XML, and begin working out some
use cases. First, locations.
Locations and Sites
-------------------
The tricky thing about locations is that if we want them to be reusable,
they can't be named. That is, we can't do:
<location name="foo">
because then 1) that location wouldn't be usable as a site root, and 2) it
wouldn't be usable under a different name. Probably what we'll have to do
is require the outermost element of a configuration file to be '<site>' (no
location name), and then have a directive to include another site at a
specific location, e.g.:
<site>
blah blah
<include-site name="foo" from="foo-site.xml"/>
In order to include another site under the specific path name. Then, we
can use named locations within a site object.
Locations and sites have some things in common. We probably want to be
able to specify, for example, that a particular permission is needed to
access a given location. So, there should be a "permission" attribute for
site, location, and include-site elements. It might also be useful to have
a "class" attribute, allowing a custom class to be used instead of the
peak.web default location class, or even to have a "configure" attribute to
allow loading .ini style configuration, but these aren't part of the
minimum requirements.
Locations and sites also probably need to have an "id" attribute taking a
property name, so that when specifying canonical absolute URLs, one can
define them relative to known location ID's, without having to know their
actual location within the app as a whole. Unfortunately, the reusability
requirement thwarts any possibility of having them be unique in the site as
a whole, so essentially they'll be like property names that are acquired
relative to a current location. In other words, if you have:
<site>
<location name="foo" id="myapp.foo">
<location name="bar" id="myapp.bar">
Then any location within the site can see the "myapp.foo" ID (unless it's
overridden in a sublocation), but only locations within /foo can see the
"myapp.bar" ID. This allows for the case where you include a sub-site more
than once to create multiple instances of something, like:
<include-site name="blog" from="notebook.xml" configure="blog.ini">
<include-site name="journal" from="notebook.xml" configure="journal.ini">
Then, any location IDs used in 'notebook.xml' will resolve to the correct
absolute URL within /blog or /journal, without affecting the other. We
should probably also have a way to publish URLs at depth greater than
one. For example, if we change our previous example to:
<site>
<offer path="foo/bar" as="myapp.bar"/>
<location name="foo" id="myapp.foo">
<location name="bar" id="myapp.bar">
Interestingly, we could allow namespace use in such paths, so you could
offer pretty much any object to be used as the target of an ID
resolution. And, we could have a '++loc++' namespace that takes namespace
ID's and looks them up, so that you could then do things like:
<offer path="++loc++myapp.foo" as="myapp.bar">
To translate one path to another. This could be very handy in integrating
web components that need to know, e.g. what page to send you to after
completing a given operation; they can just refer to the location ID, which
you can define as being a location ID from some other web component.
Pages, Views, and Permissions
-----------------------------
Okay, let's look at defining pages and views, by way of example
scenarios. Here are three ways to provide a 'foo.html' page for instances
of a class:
<page name="foo.html" for="mypkg.MyClass" resource="mypkg/foo_template"/>
<page name="foo.html" for="mypkg.MyClass" attribute="someattr"/>
<page name="foo.html" for="mypkg.MyClass" expr="mypkg.someFunc(ob)"/>
Hm. I'm not sure I like it. It seems it would be nicer to be able to say
something more like:
<content type="mypkg.MyClass" permission="security.Anybody">
<!-- public pages here -->
<page name="foo.html" resource="mypkg/foo_template"/>
<page name="bar.html" attribute="someattr"/>
</content>
<content type="mypkg.MyClass" permission="mypkg.SomePerm">
<!-- SomePerm-protected pages here -->
etc.
</content>
or better yet:
<content type="mypkg.MyClass">
<require permission="security.Anybody">
<!-- public pages here -->
<page name="foo.html" resource="mypkg/foo_template"/>
<page name="bar.html" attribute="someattr"/>
</require>
<require permission="mypkg.SomePerm">
<!-- SomePerm-protected pages here -->
etc.
</require>
</content>
I'd like to keep the format more Pythonic than Perlish; that is, there
should preferably only be one obvious way to do something. That's somewhat
in conflict with ZCML, which is very "more than one way to do it". For
example, in ZCML there are numerous directives that can simultaneously
define a wrapper class and apply permission requirements to a bunch of
attributes while also defining a view. I don't think I want that kind of
chaos. Page directives should be simple: just a name, an implementation
(resource/attribute/expression), and an optional permission override. The
"type" attribute of "content" directives should allow multiple classes or
interfaces to be specified, so you won't need to repeat yourself there.
Every page should be required to have a permission specified. It could be
on an enclosing tag, like require, content, or even the enclosing location
or site. In general, I guess permission can be "inherited" by enclosed
blocks, so a permission set at the "site" level propagates downward. If
that permission is omitted, I guess we could consider it to be
'security.Anybody', although that seems potentially dangerous. OTOH, it's
also trivial to fix: just set a more restrictive default permission, like
'myapp.User'.
The "require" and "page" elements should also be usable within "location"
and "site", and would define singleton pages of those locations:
application-level methods, in other words. It should also be possible to
use "require" to declare attributes or interfaces that are accessible with
the given permission. And, to allow use of "helper" classes for views, we
can have something like:
<content type="rfc822.Message">
<helper factory="mypkg.MailHelper" permission="mypkg.ReadMail">
<page name="summary" resource="mypkg/mail_summary"/>
to declare that the mail_summary template will be applied to an instance of
MailHelper wrapping any 'rfc822.Message' objects that are traversed to, as
long as the user has the "mypkg.ReadMail" permission. So, traversing to
"some_message_object/@@summary" in a browser will display the mail_summary
page for the message object.
Helper classes should also be usable directly on sites and locations, to
add application behaviors and thereby create "Specialists" or the
application itself.
To finish out the security permissions, we should add an "allow" attribute
to "content" and "require", that lists attributes that can be accessed with
the current permission, on the current target object type. (It might also
be nice to be able to specify an interface, rather than listing attributes,
but we can leave that to a future enhancement.)
Maybe "allow" should also be available on "helper", but I'm not
sure. Heck, maybe "helper" should be an attribute rather than an element;
that might remove some ambiguity about what permissions are applying to,
not to mention what the correct namespace traversing is. So, we'd have either:
<content type="rfc822.Message" helper="mypkg.MailHelper"
permission="mypkg.ReadMail">
<page name="summary" resource="mypkg/mail_summary"/>
or:
<content type="rfc822.Message" permission="mypkg.ReadMail">
<page name="summary" resource="mypkg/mail_summary"
helper="mypkg.MailHelper"/>
or even:
<content type="rfc822.Message" helper="mypkg.MailHelper">
<require permission="mypkg.ReadMail">
<page name="summary" resource="mypkg/mail_summary"/>
Hm. So much for only one obvious way to do it! This all gets way too
confusing when you mix it with an "allow" attribute, so we should make it
an element. For example, to allow the 'unixfrom' and 'headers' attributes
with permission ReadMail, we might say:
<content type="rfc822.Message" helper="mypkg.MailHelper">
<require permission="mypkg.ReadMail">
<allow attributes="unixfrom,headers"/>
<page name="summary" resource="mypkg/mail_summary"/>
And that fits nicely with being able to '<allow interfaces="something">'
later. To declare attribute permissions on a helper class, one should use
something like:
<content type="mypkg.MailHelper">
<allow attributes="something,otherthing"
permission="mypkg.SomePerm"/>
<allow interfaces="IFoo" permission="mypkg.FooPerm"/>
rather than wedging it into the "helped" class' declarations.
This is still an awful lot of ways to do things, but I'm not sure we can
cut back much without also hurting brevity of expression in common cases.
Specifying Objects
------------------
Rather than use ZCML's dotted path approach to finding classes and such,
I'm thinking of just using our existing technique from .ini files, of
simply importing designated modules into a current namespace and then using
eval() to get the actual objects. So, for example:
<import module="x.y.z" as="xyz"/>
would then allow you to refer to a permission class like "xyz.SomePerm", or
a content class, interface, etc., anywhere within the remainder of the
containing XML element. Any XML attributes that need to refer to an
object, class, permission, interface, or any other Python value will just
use eval. (Of course, ones that need names or lists of names will just use
the XML attribute's string value directly.)
The "as" attribute of an "import" would be optional, defaulting to the last
part of the "module" name, such that:
<import module="x.y.z"/>
is equivalent to:
<import module="x.y.z" as="z"/>
And, by default, the 'eval()' environment would be the same as it is for
.ini files, including all the (lazily-imported) peak.api modules, and handy
utilities like 'importObject'.
I think also that the "resource" attribute of "page" elements should be
able to use module import module shortcuts to refer to resource package
names. So, if you say:
<import module="x.y.z" />
<page name="foo" resource="z/foo" />
This should be interpreted in the same way as:
<page name="foo" resource="x.y.z/foo" />
"Specialists"
-------------
As we've described things so far, you create a site that actually has any
dynamic URLs, except by making them sub-URLs of a helper class. We really
need to be able to have containers that map their sub-URL to contents, the
way ZPatterns' "Specialists" do. To do that, I'm thinking we'll need some
kind of "container" element that can be used inside of "site" or "location"
elements, to specify a place for things to be looked up. Perhaps it could
be as simple as:
<location name="Members"
<container object="mypkg.MemberSource()"/>
<container object="{'nobody': mypkg.Nobody()}"/>
</location>
To define a location that first looks up its sub-URLs in a MemberSource()
instance, and then in the supplied dictionary. The target object would of
course be adapted to IWebTraversable, so the object can implement the
traversal itself, or just be a normal object supplying mapping items,
attributes, or views.
But this mapping from locations to dynamic objects needs to go both
ways. Given a content object, we need to know where its container is
located, so we can construct its absolute path. This is important, so that
we don't generate long paths that look like
"some_customer/invoices/27/lineitems/42/product" when we can just say
"/Products/927" to refer to the product in question.
A simple addition to the "content" element might just do the trick:
<content type="mypkg.Product"
location="product_id at mypkg.product_location">
...
The syntax of the container attribute would be "attr_or_view at locationID",
where "attr_or_view" is the name of an attribute or view that is looked up
on the current object, and "locationID" is a location ID used to find the
container's absolute URL. The string form of the attribute or view's value
is appended to the URL of the location specified by the location ID (plus
the ":path", if any). So, if we had this:
<location name="Products" id="mypkg.product_location">
<container
object="config.lookup(targetObj,mypkg.IProductSpecialist)"/>
</location>
in our site root, then a Product with a "product_id" attribute of 927 would
consider its URL to be "/Product/927".
Wrap-Up
=======
I think this covers the required ground pretty nicely, and with only ten
elements (shown with required attributes in (), optional attributes in [],
alternative attributes separated with '|'):
* site [id, permission, configure]
* include-site(name,from) [id, permission, configure]
* location(name) [id, permission, configure]
* container(object) [permission]
* import(module) [as]
* offer(path,as)
* content(type) [location,helper,id,permission]
* require(permission) [helper]
* allow(attributes|interfaces) [permission]
* page(name,resource|attribute|object) [helper, id, permission]
And, I think this is also fewer attributes than ZCML uses for the same
features.
In this summary, I've changed the names of a couple of attributes, and
added some "common" attributes to elements that they weren't previously
mentioned on. For example, I added "id" to "content" and "page", and added
"configure" to "location". Also, I changed <page expr=""> to <page
object=""> so it matches <container object="">.
Implementation seems relatively straightforward, at least for this small
set of elements. More to the point, the implementation of different
elements is largely orthogonal.
One open issue: I'm not sure "site" is the right name for what we're
doing. Maybe we should call it "root" instead. Or, perhaps we should just
have "location", and make the "name" attribute verboten when it appears as
the outermost element of an XML file. Heck, we could fold include-site
into location also, e.g.:
<location> <!--the root-->
<location name="foo">
stuff here
</location>
<location name="bar" include="bar-location.xml">
additional declarations here
</location>
</location>
This adds the extra bonus of being able to add declarations to an included
file. I'm not sure about how overriding would work if there are
conflicting definitions, though. I suppose we could delay making
potentially-conflicting registrations for a given location until the
location is completely defined.
Anyway, I guess that gets it down to only eight elements instead of ten,
with the definition of "location" changed to:
* location [name, id, permission, include, configure]
Extensibility doesn't look too bad, either. If we later want to define a
page as a menu item, we could do something like:
<page name="foo.html" resource="mypkg/something"
menu:id="some.menu" menu:title="Foo this Bar" />
And, almost any other sort of declaration can be added to "location" or
"content" elements or their sub-elements, accordingly.
More information about the PEAK
mailing list