package Catalyst::Plugin::StatusMessage;
{
  $Catalyst::Plugin::StatusMessage::VERSION = '1.002000';
}

use strictures 1;
use Sub::Name ();


=head1 NAME

Catalyst::Plugin::StatusMessage - Handle passing of status (success and error)
messages between screens of a web application.


=head1 SYNOPSIS

In MyApp.pm:

    use Catalyst qr/
        StatusMessage
    /;

In controller where you want to save a message for display on the next page
(here, once the "delete" action taken is complete, we are redirecting to a
"list" page to show the status [we don't want to leave the delete action in the
browser URL]):

   $c->response->redirect($c->uri_for($self->action_for('list'),
        {mid => $c->set_status_msg("Deleted widget")}));

Or, to save an error message:

   $c->response->redirect($c->uri_for($self->action_for('list'),
        {mid => $c->set_error_msg("Error deleting widget")}));

Then, in the controller action that corresponds to the redirect above:

    sub list :Path {
        my ($self, $c) = @_;
        ...
        $c->load_status_msgs;
        ...
    }

And, to display the output (here using L<Template Toolkit|Template>):

    ...
    <span class="message">[% status_msg %]</span>
    <span class="error">[% error_msg %]</span>
    ...


=head1 DESCRIPTION

There are a number of ways people commonly use to pass "status messages"
between screens in a web application.

=over 4

=item *

Using $c->stash: The stash only exists for a single request, so this
approach can leave the wrong URL in the user's browser.

=item *

Using $c->flash: The "flash" feature does provide a mechanism where the
application can redirect to an appropriate URL, but it can also lead to
a race condition where the wrong status message is displayed in the
wrong browser window or tab (and can therefore be confusing to the users
of your application).

=item *

Query parameters in the URL: This suffers from issues related to
long/ugly URLs and leaves the message displayed even after a browser
refresh.

=back


This plugin attempts to address these issues through the following mechanisms:

=over 4

=item *

Stores messages in the C<$c-E<gt>session> so that the application is free
to redirect to the appropriate URL after an action is taken.

=item *

Associates a random 8-digit "token" with each message, so it's completely
unambiguous what message should be shown in each window/tab.

=item *

Only requires that the token (not the full message) be included in the
redirect URL.

=item *

Automatically removes the message after the first time it is displayed.
That way, if users hit refresh in their browsers they only see the
messages the first time.

=back


=head1 METHODS


=head2 load_status_msgs

Load both messages that match the token parameter on the URL (e.g.,
http://myserver.com/widgits/list?mid=1234567890) into the stash
for display by the viewer.

In general, you will want to include this in an C<auto> or "base" (if
using Chained dispatch) controller action.  Then, if you have a
"template wrapper page" that displays both "C<status_msg>" and
"C<error_msg>", you can automatically and safely send status messages to
any related controller action.


=head1 CONFIGURABLE OPTIONS


=head2 session_prefix

The location inside $c->session where messages will be stored.  Defaults
to "C<status_msg>".


=head2 token_param

The name of the URL param that holds the token on the page where you
want to retrieve/display the status message.  Defaults to "C<mid>".


=head2 status_msg_stash_key

The name of the stash key where "success" status messages are loaded
when C<$c-E<gt>load_status_msgs> is called.  Defaults to C<status_msg>.


=head2 error_msg_stash_key


The name of the stash key where error messages are loaded when
C<$c-E<gt>load_status_msgs> is called.  Defaults to C<error_msg>.


=head2 Configuration Example

Here is a quick example showing how Catalyst::Plugin::StatusMessage
can be configured in 


    # Configure Catalyst::Plugin::StatusMessage
    __PACKAGE__->config(
        'Plugin::StatusMessage' => {
            session_prefix          => 'my_status_msg',
            token_param             => 'my_mid',
            status_msg_stash_key    => 'my_status_msg',
            error_msg_stash_key     => 'my_error_msg',
        }
    );


=head1 INTERNALS

Note: You normally shouldn't need any of the information in this section
to use L<Catalyst::Plugin::StatusMessage>.


=head2 get_error_msg

A dynamically generated accessor to retrieve saved error messages

=cut


=head2 get_status_msg

A dynamically generated accessor to retrieve saved status messages

=cut


=head2 set_error_msg

A dynamically generated accessor to save error messages

=cut


=head2 set_status_msg

A dynamically generated accessor to save status messages

=cut


=head2 _get_cfg

Subref that handles default values and lets them be overriden from the MyApp
configuration.

=cut

my $_get_cfg = sub {
    my ($self) = @_;

    my %config = (
        session_prefix       =>  'status_msg',
        token_param          =>  'mid',
        msg_types            =>  [ qw(status error) ],
        status_msg_stash_key =>  'status_msg',
        error_msg_stash_key  =>  'error_msg',
        %{$self->config->{"Plugin::StatusMessage"} || {}}
    );
    \%config;
};


=head2 get_status_message_by_type

Fetch the requested message type from the user's session

=cut

sub get_status_message_by_type {
    my ($self, $token, $conf, $type) = @_;

    return delete($self->session->{$conf->{session_prefix}}{$type}{$token})||'';
}


=head2 set_status_message_by_type

Save a message to the user's session

=cut

sub set_status_message_by_type {
    my ($self, $conf, $type, $value) = @_;

    my $token = int(rand(90_000_000))+10_000_000;
    $self->session->{$conf->{session_prefix}}{$type}{$token} = $value;
    return $token;
}


=head2 load_status_msgs

Load both messages that match the token param (mid=###) into the stash
for display by the view.

=cut

sub load_status_msgs {
    my ($self) = @_;

    my $conf = $self->$_get_cfg;

    my $token  = $self->request->params->{$conf->{token_param}} || return;

    $self->stash(
        map +(
            $conf->{"${_}_msg_stash_key"}
                =>  $self->get_status_message_by_type($token, $conf, $_)
        ), @{$conf->{msg_types}}
    );
}


=head2 make_status_message_get_set_methods_for_type

Called at startup to install getters and setters for each type of
message (status & error)

=cut

sub make_status_message_get_set_methods_for_type {
    my ($pkg, $type) = @_;

    # Make getter for messages of $type
    my $get_name = "${pkg}::get_${type}_msg";
    my $get = Sub::Name::subname($get_name, sub {
        my ($self, $token) = @_;
        $self->get_status_message_by_type($token, $self->$_get_cfg, $type);
    });
    # Make getter for messages of $type
    my $set_name = "${pkg}::set_${type}_msg";
    my $set = Sub::Name::subname($set_name, sub {
        my ($self, $value) = @_;
        $self->set_status_message_by_type($self->$_get_cfg, $type, $value);
    });
    # Install getter and setter into class
    {
        no strict 'refs';
        *{$get_name} = $get;
        *{$set_name} = $set;
    }
    return;
}


# Add class methods to save/retrieve messages for status & error message types 
__PACKAGE__->make_status_message_get_set_methods_for_type($_) for qw(status error);


=head1 AUTHOR

Kennedy Clark, [email protected]


With many thanks to Matt Trout (MST) for coaching on the details of Catalyst
Plugins and for most of the magic behind the current implementation.


=head1 COPYRIGHT

This library is free software. You can redistribute it and/or modify it under
the same terms as Perl itself.


=cut

1;