NAME

CatalystX::ConsumesJMS - role for components providing Catalyst actions consuming messages

VERSION

version 1.0

SYNOPSIS

package MyApp::Base::Stuff;
use Moose;
extends 'Catalyst::Component';
with 'CatalystX::ConsumesJMS';

sub _kind_name {'Stuff'}
sub _wrap_code {
  my ($self,$c,$destination_name,$msg_type,$route) = @_;
  my $code = $route->{code};
  my $extra_config = $route->{extra_config};
  return sub {
    my ($controller,$ctx) = @_;
    my $message = $ctx->req->data;
    $self->$code($message);
  }
}

Then:

package MyApp::Stuff::One;
use Moose;
extends 'MyApp::Base::Stuff';

sub routes {
  return {
    my_input_destination => {
      my_message_type => {
        code => \&my_consume_method,
        extra_config => $whatever,
      },
      ...
    },
    ...
  }
}

sub my_consume_method {
  my ($self,$message) = @_;

  # do something
}

Also, remember to tell Catalyst to load your Stuff components:

<setup_components>
 search_extra [ ::Stuff ]
</setup_components>

DESCRIPTION

This role is to be used to define base classes for your Catalyst-based JMS / STOMP consumer applications. It's not to be consumed directly by application components.

Routing

Subclasses of your component base specify which messages they are interested in, by writing a routes sub, see the synopsis for an example.

They can specify as many destinations and message types as they want / need, and they can re-use the code values as many times as needed.

The main limitation is that you can't have two components using the exact same destination / type pair (even if they derive from different base classes!). If you do, the results are undefined.

It is possible to alter the destination name via configuration, like:

<Stuff::One>
 <routes_map>
  my_input_destination the_actual_destination_name
 </routes_map>
</Stuff::One>

The "code"

The hashref specified by each destination / type pair will be passed to the "_wrap_code" function (that the consuming class has to provide), and the coderef returned will be installed as the action to invoke when a message of that type is received from that destination.

The action, like all Catalyst actions, will be invoked passing:

  • the controller instance (you should rarely need this)

  • the Catalyst application context

You can do whatever you need in this coderef, but the synopsis gives a generally useful idea. You can find more examples of use at https://github.com/dakkar/CatalystX-StompSampleApps

Required methods

_kind_name

As in the synopsis, this should return a string that, in the names of the classes deriving from the consuming class, separates the "application name" from the "component name".

These names are mostly used to access the configuration.

_wrap_code

This method is called with:

  • the Catalyst application as passed to register_actions

  • the destination name

  • the message type

  • the value from the routes corresponding to the destination name and message type slot (see "Routing" above)

The coderef returned will be invoked as a Catalyst action for each received message, which means it will get:

  • the controller instance (you should rarely need this)

  • the Catalyst application context

You can get the de-serialized message by calling $c->req->data. The JMS headers will most probably be in $c->req->env (or $c->engine->env for older Catalyst), all keys namespaced by prefixing them with jms.. So to get all JMS headers you could do:

my $psgi_env = $c->req->can('env')
               ? $c->req->env
               : $c->engine->env;
my %headers = map { s/^jms\.//r, $psgi_env->{$_} }
              grep { /^jms\./ } keys $psgi_env;

You can set the message to serialise in the response by setting $c->stash->{message}, and the headers by calling $c->res->header (yes, incoming and outgoing data are handled asymmetrically. Sorry.)

Implementation Details

HERE BE DRAGONS.

This role should be consumed by sub-classes of Catalyst::Component.

The consuming class is supposed to be used as a base class for application components (see the "SYNOPSIS", and make sure to tell Catalyst to load them!).

Since these components won't be in the normal Model/View/Controller namespaces, we need to modify the COMPONENT method to pick up the correct configuration options. This is were "_kind_name" is used.

We hijack the expand_modules method to generate various bits of Catalyst code on the fly.

If the component has a configuration entry enabled with a false value, it is ignored, thus disabling it completely.

We generate a controller package for each destination, by calling "_generate_controller_package", and we add an after method modifier to its register_actions method inherited from Catalyst::Controller, to create the actions for each message type. The modifier is generated by calling "_generate_register_action_modifier".

_generate_controller_package

my $pkg = $self->_generate_controller_package(
              $appname,$destination,
              $config,$route);

Generates a controller package, inheriting from whatever "_controller_base_classes" returns, called ${appname}::Controller::${destination_name}. Any roles returned by "_controller_roles" are applied to the controller.

Inside the controller, we set the namespace config slot to the destination name.

_controller_base_classes

List (not arrayref!) of class names that the controllers generated by "_generate_controller_package" should inherit from. Defaults to 'Catalyst::Controller::JMS'.

_controller_roles

List (not arrayref!) of role names that should be applied to the controllers created by "_generate_controller_package". Defaults to the empty list.

_generate_register_action_modifier

my $modifier = $self->_generate_register_action_modifier(
                 $appname,$destination,
                 $controller_pkg,
                 $config,$route);

Returns a coderef to be installed as an after method modifier to the register_actions method. The coderef will register each action with the Catalyst dispatcher. Each action will have the attribute MessageTarget, see Catalyst::Controller::JMS.

Each action's code is obtained by calling "_wrap_code".

AUTHOR

Gianni Ceccarelli <gianni.ceccarelli@net-a-porter.com>

COPYRIGHT AND LICENSE

This software is copyright (c) 2012 by Net-a-porter.com.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.