March 02, 2005

More OI2 design fun: ActionResolvers and REST URLs

OIN-135 says: "Allow REST-style URLs" and for what you'd think is a relatively core change (how the application server gets data from the outside world) it was a fairly simple change to make. I'll talk about the first part today (pulling the data out); tomorrow we'll cover how those data get assigned to actions.

First, an update on my earlier post about resolving URLs to actions. Fortunately these changes worked out exactly like I'd planned. Previously the controller instantiated the action from the URL itself. Now it delegates the job to an ActionResolver class which itself just asks each of a set of objects collected at runtime if they can resolve the URL to an Action object. First one to resolve wins. Classic chain of responsibility. So the controller now just does this:

my $action = OpenInteract2::ActionResolver->get_action( $request );

And in that method (condensed):

sub get_action {
    my ( $class, $request ) = @_;
    my $url = $request->url_relative;
    my ( $action );
    foreach my $r ( $class->get_all_resolvers ) {
        $action = eval { $r->resolve( $request, $url ) };
        if ( $@ ) {
            $log->warn( "Resolver ", ref( $r ), " threw an ",
                        "exception ($@); continuing with others..." );
        last if ( $action );
    return $action;

So that call to resolve() in the foreach is where all the work is getting done. Here's the initial version of the 'resolve()' function for the workhorse link in the chain -- we grab the action from the URL see if an action by that name exists. If so we assign the task from the URL to it and return the object (condensed):

sub resolve {
    my ( $self, $request, $url ) = @_;
    my ( $action_name, $task_name ) = OpenInteract2::URL->parse( $url );
    return unless ( $action_name );
    my $action = eval {
        CTX->lookup_action( $action_name )
    if ( $@ ) {
        $log->warn( "Caught exception from context trying to lookup ",
                    "action '$action_name': $@" );
    if ( $task_name ) {
        $action->task( $task_name );
    return $action

Pretty simple. Getting back to our original point: we now want to add REST parameters. Since we've already done the work of decoupling URL parsing and URL resolution, and URL resolution to action, the job was actually pretty easy.

First, modify the code in OpenInteract2::URL->parse() to return not only an 'action' and 'task' given a URL, but everything else as parameters. (I won't show that here.) So given a URL 'http://foo/news/archive/2005/12' the URL parsing will break that down into an action ('news'), task ('archive') and two parameters ('2005' and '12').

So now modify our return values from parse() to accommodate these parameters:

    my ( $self, $request, $url ) = @_;
    my ( $action_name, $task_name, @params ) = OpenInteract2::URL->parse( $url );
    return undef unless ( $action_name );

Next, add a request property so that any component from the app server can get these additional parameters -- we'll call it param_url_additional().

Finally, in the OpenInteract2::ActionResolver parent class implement a common method for assigning these:

sub assign_additional_params_from_url {
    my ( $self, $request, @params ) = @_;
    if ( scalar @params ) {
        $log ||= get_logger( LOG_ACTION );
        $log->info( "Assigning additional URL parameters: ",
                    join( ', ', @params ) );
        $request->param_url_additional( @params );

And finally add a call to this parent method from our resolve(): just before we return

    $self->assign_additional_params_from_url( $request, @params );
    return $action;

Here's another example of a resolver with the same functionality implemented. OpenInteract2::ActionResolver::UserDir resolves URLs like '/~cwinters/' so we don't have to use the uglier '/user/display/?user_id=5':

sub resolve {
    my ( $self, $request, $url ) = @_;
    return unless ( $url =~ m|^/?~| );
    # cleanup url to known state
    $url =~ s|^/||; $url =~ s/\?.*$//; $url =~ s|/$||;
    my ( $username, $task, @params ) = split /\//, $url;
    # /~user same as /~user/display
    $task ||= 'display';
    my $action = CTX->lookup_action( 'user' );
    $action->task( $task );
    $action->param( login_name => $username );
    $self->assign_additional_params_from_url( $request, @params );
    return $action;

Tomorrow: fine, now we've pulled the REST parameters out. What else can we do with them?

Next: New toy: ipod shuffle
Previous: Software I'm scared to upgrade