[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