During the m5 milestone, there were at least three schema problems that I know of, that shared a common root cause:

* AbstractCollection.subscribers - it was desired that this be a bidirectional reference attribute, but there was then no meaningful place to put the "other end" of the relationship, without creating a dummy abstract class like "Subscriber" to act as a mixin.

* AbstractCollection.color - it was desirable for certain parts of the system to associate a color with collections, but this required modifying the AbstractCollection base class to add the attribute, even though collections in the abstract don't have color. :)

* sharing.UIDMap.items - this was a collection mapping iCal UIDs to calendar events, but to be a bidirectional reference, it needed an inverse attribute on CalendarEventMixin, thereby creating an undesirable circular dependency between osaf.pim.calendar and osaf.sharing.

In addition to these three specific issues, there also have been occasional problems with people getting "cannot be modified after use" errors, or other errors having to do with setting up bidirectional relationships across parcels or between more than two kinds.

The common cause of all of these problems is that there is currently no easy way for a parcel to add attributes to existing kinds, without modifying the code of the existing kind to refer to the new kind. This problem also affects third-party extenders of Chandler. For example, suppose somebody wants to create an "accessibility" parcel that allows assigning sounds to collections instead of colors? :)


So, here's what I'd like to propose:

1. Allow defining "anonymous" inverse relationships. If we wanted 'AbstractCollections.subscribers' to be a bidirectional reference, we would need only do this:

    # create an unordered, many-to-many relationship with Item
    subscribers = schema.Many(inverse=schema.Many())

Similarly, the sharing.UIDMap.items attribute could be defined with:

    # create an ordered many-to-one relationship
    items = schema.Sequence(pim.CalendarEventMixin, inverse=schema.One())

2. In the event that something like an attribute editor (or some other object or API that needs an attribute name) needs to be pointed at one of these "anonymous" attributes, they will be accessible via a "fully qualified" attribute name. For example, to access the collections an item is subscribed to, you could get its "osaf.pim.AbstractCollection.subscribers.inverse" attribute. You can't get this attribute statically in Python code; you have to use getattr() or getAttributeValue() or any of the other normal APIs that take attribute names, and pass in the string "osaf.pim.AbstractCollection.subscribers.inverse".

3. Implement a convenience API that lets you use the .inverse directly, in place of using the long name, e.g. something like this:

    pim.AbstractCollection.subscribers.inverse(someObject)

could perhaps be used to get the attribute in a more type-safe way. If the attribute is frequently used in a given module, it can do something like this at the top of the module to create a shortcut:

    subscribees_of = pim.AbstractCollection.subscribers.inverse

and then just use it directly:

    # returns the collections someObject subscribes to
    subscribees_of(someObject)

4. For the case where both ends of a relationship already exist (e.g. AbstractCollection and ColorType), we would allow defining the necessary attributes as follows:

    class CollectionColor(schema.Relationship):
        collections = schema.Many(pim.AbstractCollection)
        color = schema.One(blocks.ColorType)

Defining this class would create "anonymous" attributes on the relevant kind(s). If one side of the relationship is a type (e.g. ColorType), then this would just create an anonymous value attribute on the kind (e.g. a "CollectionColor.color" attribute on the AbstractCollection kind).

If both sides of the relationship are kinds, however, then each gets an attribute that points to the other, creating a bidirectional reference without modifying either kind's class definition. For example, if a third party parcel wanted to create a "likes" relationship between contacts, it might do:

    class Likes(schema.Relationship):
        likees = schema.Many(pim.Contact)
        likers = schema.Many(pim.Contact)

This would create a many-to-many relationship between contacts, *without* requiring the base Contact type to be modified. However, it will not conflict with any other third-party extension that creates such attributes, nor will it be affected if Chandler later adds "likees" and "likers" attributes to Contact. This is because the attribute names created by the above code will be "some_parcel.Likes.likees" and "some_parcel.Likes.likers", and these names will therefore not conflict with a "likees" or "likers" that might be defined by some other parcel.

To navigate this relationship, you would simply use the likes and isLikedBy class attributes, as before:

    Likes.likees(somebody)      # get the contacts who somebody likes
    Likes.likers(somebody)      # get the contacts who like somebody
    me in Likes.likees(you)     # do you like me?

    Likes.likees(everybody).add(somebody)  # everybody likes somebody!
    Likes.likees(me).remove(you)           # I don't like you any more

As you can see, this provides a fairly usable API for parcels that create new relationships; it's not quite as convenient as being able to say 'somebody.likees' directly, but it doesn't require the Chandler-supplied schema to anticipate every possible future need, and it doesn't create circular dependencies between parcels.

Some of you may remember ideas like these from my Spike prototype earlier this year, so these are not really anything new. There's even a documented implementation of them in Spike; see:

    
http://svn.osafoundation.org/chandler/trunk/internal/Spike/src/spike/schema.txt

and there's some additional discussion in:

    
http://svn.osafoundation.org/chandler/trunk/internal/Spike/src/spike/overview.txt

But at the time I was creating the Spike-like schema API for Chandler, it was not at all clear to me how I could implement these ideas using the repository, and also the need for them didn't seem to be an immediate issue. Now, however, the need has popped up repeatedly, and with Andi's help I've figured out a basic idea for how to make more or less the same API work atop the repository's schema mechanisms.

I'd like to hear your comments and questions, to make sure this is going in the right direction. By the way, I believe these changes should also allow us to get rid of some of the annoying schema errors we currently get that result from circular dependencies, including the dreaded "cannot be modified after use" error, and we might also be able to get rid of the confusing distinction between 'otherName' and 'inverse', as 'otherName' is essentially only needed right now as a workaround for the absence of the API features I've described here.
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

Open Source Applications Foundation "Dev" mailing list
http://lists.osafoundation.org/mailman/listinfo/dev

Reply via email to