All of Rum’s functionallity is accesible through the rum.RumApp active instance which is always accesible at the rum.app symbol (which is a thread-safe, stackable-context proxy that delegates to the active app).
The most significant relations between components are depicted in the following figure.
The basic pattern here is object composition and this is how a Rum application is configured with the appropiate components.
These components are injected at runtime based on the setup information stored at rum.RumApp.config which is derived from one passed as the config parameter to rum.RumApp s contructor.
The lazy-loading of components is done by the rum.component.Component descriptor.
The following sequence diagram depicts how the different components cooperate when responding to a CRUD request.
In the above diagram the ‘edit’ action is being performed on the member of a collection, perhaps from a request whose URL was something like /persons/5/edit
As you can see, once a request arrives Rum uses the rum.interfaces.IRouter implementation to match the incoming URL and REQUEST_METHOD and produce a routes dict. The routes dict holds the neccesary information to call each of the factories to produce an appropiate product which is used to handle the domain model we’re being asked to handle.
Then rum.RumApp asks the ControllerFactory for a controller to handle some Person resource (the factory might decide to return different controllers depending on routing args in the routes dict, but by default only the rum.CRUDController is used for all CRUD operations for any kind of resource)
rum.RumApp will then forward the request to the controller instance the factory created and the controller will then ask for a rum.Repository to the rum.RepositoryFactory which knows how to handle our imaginary Person.
The repository is in charge of all CRUD operations of a collection and its members. This abstraction allows the same controller (and view factory since the model’s metadata is abstracted into rum.fields.Field instances the repository, with a custom rum.FieldFactory, knows how to introspect) can be used with any kind of resource.
Once the member is fetched the controller will ask the rum.ViewFactory for a rum.interfaces.IView and pass control to the template which will render Rum’s chrome and use the view the controller gave it to render the item it has retrieved from the repository.
We can see that the view calls back to rum.RumApp to generate some urls to render links and it even asks the repository factory for a repository to handle a related class (perhaps to render a drop down menu where the user can choose a Group where this person belongs to in a many to one relationship.
Some might be waving their hands in despair now... isn’t this breaking the separation between the view and model layers? IMHO it isn’t since the view doesn’t know anything about the model really, it is the repository who knows the details. The view only knows how to talk to repositories but not how these might do their business. Besides that, if the controller was in charge of fetching all this stuff before sending it to the view it might be doing (much) more work than it should if the view doesn’t need that information (for a JSON response, for example)
Content negotitation is done by the controller in a way inspired by turbogears expose() decorator but, in the case of TG1, much simpler thanks to the fact that controller instances are created and destroyed for every request (this one comes from Pylons) and can hold some state (much of the complexity from TG1’s expose() is caused because state has to be passed as function parameters among decorators which decorate the same function).
Instead of using decorators Rum uses rum.genericfunctions.around(), rum.genericfunctions.before() and rum.genericfunctions.after() methods (which are, in a certain way, decorators too) that wrap two methods which were designed with this mind: rum.Controller.call_action() and rum.Controller.process_output().
rum.Controller.call_action() is wrapped with before methods that are run when certain conditions are met (eg: routes['format'] == 'json') and these set the correct Content-Type header on the response that is being generated (which is attached to the controller instance)
rum.Controller.process_output() is extended with when methods that are called when the controller action returned a dict and can act accordingly based on the format we’ve been asked to represent the resource as: ‘json’ will send the dict through turbojson, ‘html’ will find an appropiate template and send the dict as the namespace, ‘rss’ could simply send the dict to another template, ‘pdf’ could call process_output recursively but pass ‘html’ as the format and then send the output through Pisa, etc...
In fact, these extra formats can be added cleanly by using mixin classes that declare the needed rules inside their body (the “ugly hack” approach could simply extend the generic function without the mixin class limiting the scope therefore affecting every single controller subclass which happens to be loaded in the same process). The user can then enable these output formats by adding a rule to the controller factory that returns a controller subclass which inherits from this mixin if ceratin config option is enabled (or whatever that can be checked for in a dispatch rule).
The final twist could be to do this dynamically by creating subclasses on the fly which inherit from the mixin and the controller class that the next_method returns, probably too magical and mind-twisting at this point though... anyway, peak.rules gives you that power, it’s up to you what you do with it.
Pay special attention to the lifetime of each of the objects (the cross at the end of the activation frame of each participant) because this is what determines which objects are thread-safe to modify when a request is taking place and which aren’t (and I’ve taken great care to make it as accurate as possible)
All the factories and Router (in general: all components that are bound to rum.RumApp and loaded via rum.component.Component) live as long as the app is not garbage-collected. It is not thread-safe to modify these once the app has been finalized (by calling the rum.wsgiapp.RumApp.finalize()). It is thread-safe though to alter them before finalizing the app as long as the app instance is not shared among threads before feeding it to a server (the usual case when creating the app inside a factory function or module scope)
However, all the products of these factories are GCed as soon as the request is over (except rum.fields.Field instances which will be cached). Some factories might decide to cache their products too (eg: tw.rum.WidgetFactory since widgets cannot be modified once they have been intialized) so take please confirm with the rum.interfaces.IViewFactory you’re using.
Generally, only rum.Controller and rum.Repository instances can be safely altered during a request and these take advantage of this fact to set some internal state in the instance through a series of rum.genericfunctions.before() methods depending on the request’s context.