http://qs321.pair.com?node_id=136784
Category: Text Processing
Author/Contact Info John Clyman (module-support@clyman.com)
Description: When working on large-ish projects, I've sometimes found it gets to be a real pain to manage all the error messages, status messages, and so on that end up getting scattered throughout my code. I wrote MessageLibrary to provide a simple OO way of generating messages from a centralized list of alternatives, so you can keep everything in one easy-to-maintain place.
package MessageLibrary;
$VERSION = "0.12";

# Copyright (C) 2002 John Clyman (module-support@clyman.com)
# Documentation and licensing details in POD below, or use perldoc Mes
+sageLibrary

=head1 NAME

MessageLibrary - create objects with methods you can call to generate 
+both static and dynamic status, error, 
                 or other messages

=head1 SYNOPSIS

  # create a MessageLibrary
    $error_messages = MessageLibrary->new({
      bad_file_format  => 'File format not recognized!',
      file_open_failed => sub{"Unable to open file $_[0]: $!"},
      _default         => sub{"Unknown message " . shift() . 
                              " with params " . (join ",",@_)},
    });

  # generate messages
    print $error_messages->bad_file_format;           
    print $error_messages->file_open_failed('myfile');
    print $error_messages->no_such_message;  # falls back to _default

  # override default prefixes and suffixes
    $error_messages->set_prefix("myprogram: ");
    $error_messages->set_suffix("\n");

=head1 DESCRIPTION

=head2 Overview

With the MessageLibrary class, you can create objects that dynamically
+ construct status, error, or other messages
on behalf of your programs. MessageLibrary is particularly useful in l
+arger projects, where it can be used to
create centralized collections of messages that are easier to maintain
+ than string literals scattered throughout
the code.

To create a MessageLibrary object, you'll need to create a hash contai
+ning a set of keywords and a message 
associated with each keyword, then pass that hash to the C<new> constr
+uctor.
The keywords you choose are then exposed as methods of an individual M
+essageLibrary object, so you can generate messages
with this syntax:

  $messages->message_keyword(...with params too, if you want...)

The messages themselves may be either literal strings or anonymous sub
+routines that can perform arbitrarily complex
operations. For instance, if you create an C<$error_messages> object l
+ike this:

  $error_messages = MessageLibrary->new({
    file_open_failed => sub{"Unable to open file $_[0]: $!\n"}
  });

You can then write this:

  open INPUT, "/no/such/file" 
    or die $error_messages->file_open_failed('myfile');

And get this result:

  Unable to open file myfile: No such file or directory

Notice that parameters to the method call are accessible to your subro
+utine via C<@_>, and that
the global C<$!> variable containing the error message from the last f
+ile operation is available too.

When you're using static error messages -- i.e., where interpolation a
+t the moment of message generation is not 
required -- you can skip the anonymous subroutine and simply provide a
+ string literal:

  $status_messages = MessageLibrary->new(
    new_record => 'loading new record',
    all_done   => 'processing complete',
  );
  ...
  print $status_messages->new_record;
  ...
  print $status_messages->all_done;

=head2 Prefixes and Suffixes

Whether you're using static or dynamic messages, there's actually one 
+more thing that MessageLibrary objects
do when constructing messages: They add a prefix and a suffix. By defa
+ult, the prefix contains the name of the current
executable (stripped of path information if you're running on a Window
+s or Unix variant), and the suffix is simply
a newline. So in practice you'll normally get messages that look more 
+like this:

  YourProgramName: Unable to open file myfile: No such file or directo
+ry\n

You can change this behavior by calling the C<set_prefix> and C<set_su
+ffix> methods:

  $error_messages->set_prefix("Error: ");
  $error_messages->set_suffix(".");

which would result instead in:

  Error: Unable to open file myfile: No such file or directory.

The prefix and suffix that you set apply to all messages emitted by an
+ individual MessageLibrary object.

(Incidentally, you can retrieve the current prefix and suffix by using
+ the C<get_prefix> and C<get_suffix> methods,
but I can't think of a particularly compelling reason to actually do t
+hat.)

=head2 Defining Fallback Messages

What happens if you try to call a method for which no message was defi
+ned? MessageLibrary provides default behavior, so that:

  print $status_messages->no_such_message('nice try', 'dude');

results in:

  YourProgramName: message no_such_message(nice try,dude)\n

You can override this behavior by specifying a C<_default> key (and as
+sociated message) in your constructor:

  $error_messages = MessageLibrary->new({
    bad_file_format => 'File format not recognized!',
    _default => sub{"Unknown message '$_[0]' received"},
  });

With this C<_default> definition, the output would instead be:

  YourProgramName: Unknown message 'no_such_message' received\n

=head2 Practical Uses

If you have a fairly large, multi-module program, you may want to cent
+ralize many of your messages in a single module
somewhere. For example:

  package MyMessages;
  @ISA = qw(Exporter);
  @EXPORT = qw($error_messages $status_messages);
  use vars qw($error_messages $status_messages);
  use MessageLibrary;
  use strict;
  
  {
    my $verbose = 1;

    $error_messages = MessageLibrary->new(
      file_open => sub {return qq{file open failed on $_[0]: $!}},
      _default  => sub {return "unknown error $_[0] reported"},
    );

    $status_messages = MessageLibrary->new(
      starting_parser    => ($verbose ? "Starting parser\n" : ""),
      starting_generator => ($verbose ? sub {"Starting generator $_[0]
+\n"} : ""),
    );
    $status_messages->set_prefix();
    $status_messages->set_suffix();

    1;
  }

Then your other modules can simply C<use MyMessages> and do things lik
+e:

  print $status_messages->starting_parser;
  print $status_messages->starting_generator('alpha');
  print $status_messages->starting_generator('omega');
  print $error_messages->unexpected_end_of_file;

Since all your messages are located in one module, it's a simple task 
+to change their wording -- or even language,
though this package is not really intended as a substitute for somethi
+ng like Locale::Maketext -- control their level 
of verbosity, and so on.

Note that the methods generated are unique to each MessageLibrary obje
+ct, so that given the definitions above, this
statement:

  print $status_messages->file_open('my_file');

would end up calling the C<_default> message generator for the C<$stat
+us_messages> object. (C<file_open> was defined
only in the constructor for C<$error_messages>, so no C<file_open> met
+hod exists for C<$status_messages>.) In effect, the method-call
syntax is merely syntactic sugar for a hypothetical method call like t
+his:

  print $status_messages->generate_message('file_open','my_file');   #
+ not for real

On a separate note, if you wish to subclass MessageLibrary, you can ov
+erride the default (empty) C<_init> function that the
constructor calls and perform further initialization tasks there.

=head2 Performance Considerations

Not surprisingly, encapsulating your message generation within an obje
+ct -- and, sometimes, an anonymous
subroutine -- exacts a performance penalty. I've found in small-scale 
+experiments that the method call and anonymous-subroutine execution 
is roughly an order of magnitude slower than using literal strings and
+ Perl's native interpolation. But it's still 
I<pretty> fast in most cases, and the reduced speed may be an acceptab
+le tradeoff for improved maintainability,
particularly when it comes to things like error messages that are (we 
+hope!) generated only infrequently.

=head2 Potential Enhancements

There's currently no way to modify or add messages once you've constru
+cted the object, nor a clone/copy method,
but I haven't yet found a reason to implement either capability.

=cut


######################################## CODE STARTS HERE ############
+############################

use vars qw($AUTOLOAD);
use strict;
use warnings;
use Carp;


=head1 PUBLIC METHODS

=over 4

=item MessageLibrary->new(\%keyword_message_hash);

Construct a new MessageLibrary object. The (key,value) pairs in C<%key
+word_message_hash> are used to define
the methods that the object will expose and the messages that will be 
+generated when those methods are called.
The keys should be names that would pass muster as Perl subroutine nam
+es, because you'll likely be calling
them using the OO arrow syntax:

  $message_library->method_name;

The values (messages) may be either literal strings or blocks of code 
+to be interpreted each time the method
is invoked. Parameters passed to the method are accessible to the code
+ block in C<@_> as if it were a normal
subroutine. For example:

  $status_message = MessageLibrary->new(
    {general => sub{"You said: $_[0], $_[1], $_[2]."}};
  );
  print $status_message->general('zero', 'one', 'two');

results in:

  You said: zero, one, two.

The key C<_default> has a special significance: It defines a message t
+hat is used if an unknown method is called.
In this case, C<$_[0]> contains the name of the unknown method, and th
+e rest of C<@_> contains the parameters.
The object will provide standard C<_default> behavior if no such key i
+s explicitly provided.

=cut

sub new {
  my ($class, @args) = @_;
  my $self = {};
  bless $self, $class;
  $self->_init(@args);  # do the real work
  return $self;
}


=item $messages->set_prefix($new_prefix)

Set the prefix that is prepended onto any message returned by this obj
+ect. By default, the prefix contains 
the name of the current executable (with path stripped out if you're r
+unning under Windows and *nix OSs).

Omitting C<$new_prefix> is equivalent to specifying a null string.

=cut

sub set_prefix {
  croak "set_prefix expects a single optional param" unless @_ <= 2;
  my ($self, $prefix) = @_;
  $prefix = '' unless defined($prefix);
  $self->{prefix} = $prefix;
  return 1;
}


=item $messages->set_suffix($new_suffix)

Set the suffix that is appended onto any message returned by this obje
+ct. By default, the suffix is a newline.

Omitting C<$new_suffix> is equivalent to specifying a null string.

=cut

sub set_suffix {
  croak "set_suffix expects a single optional param" unless @_ <= 2;
  my ($self, $suffix) = @_;
  $suffix = '' unless defined($suffix);
  $self->{suffix} = $suffix;
  return 1;
}


=item $messages->get_prefix

Return the currently defined prefix.

=cut

sub get_prefix {
  croak "get_prefix expects no params" unless @_ == 1;
  return $_[0]->{prefix};
}


=item $messages->get_suffix

Return the currently defined suffix.

=cut

sub get_suffix {
  croak "get_suffix expects no params" unless @_ == 1;
  return $_[0]->{suffix};
}

=back

=head1 PRIVATE METHODS (AND VARIABLES)

=over 4

=item AUTOLOAD

The AUTOLOAD method is called whenever a MessageLibrary object receive
+s a method call to generate a message. It
does not cache methods in the symbol table for future access, because 
+methods are unique to I<individual>
MessageLibrary objects. (Remember that we're using method calls merely
+ as syntactic sugar to make the calling code
more readable.)

=cut

sub AUTOLOAD {
  my $self = shift;
  $AUTOLOAD =~ /.*::(\w+)/;
  my $message_name = $1;                                             #
+ get name of method
  return if $message_name eq 'DESTROY';                              #
+ ignore destructor call
  
  my $message_generator = $self->{messages}->{$message_name};        #
+ look up generator for this method
  if (!defined($message_generator)) {                                #
+ doesn't exist?
    $message_generator = $self->{messages}->{_default};              #
+ use _default generator
    @_ = ($message_name, @_);                                        #
+ push method name back onto params list
  }

  my $prefix = $self->get_prefix();
  my $suffix = $self->get_suffix();

  if (ref $message_generator eq 'CODE') {
    return $prefix . (&$message_generator) . $suffix;                #
+ evaluate code...
  } else {
    return $prefix . $message_generator . $suffix;                   #
+ ...or just use static message text
  }
}


=item $messages->_init(@_)

C<_init> does the actual initialization.

=cut

sub _init {
  my ($self, @params) = @_;
  my %message_hash = defined $_[1] ? %{$_[1]} : ();     # dereference 
+hash or provide empty hash
  my %messages = (
    _default => sub {return "message " . $_[0] . "(" . (join ",", @_[1
+..$#_]) . ")"},    # default value for _default
    %message_hash
  );
  $self->{messages} = \%messages;
  
  my $prefix = $0;                                      # get name of 
+executable
  if ($^O eq 'MSWin32') {                               # Windows?
    $0 =~ m{(\\|\A)([^\\]*)$};                          #   get stuff 
+after last backslash
    $prefix = $2;
  } elsif ($^O ne 'Mac' && $^O ne 'VMS' && $^O ne 'OS2') { # i.e., it'
+s *nix
    $0 =~ m{(/|\A)([^/]*)$};                           #   get stuff a
+fter last forward slash
    $prefix = $2;
  }
  $self->set_prefix("$prefix: ");
  $self->set_suffix("\n");
}

=item Internal Data Structure

A MessageLibrary is a blessed hash containing the following keys:

=over 4

=item messages

A reference to the hash containing message keywords and message text/c
+ode that was passed into the constructor.

=item prefix

The current prefix, set with C<set_prefix>.

=item suffix

The current suffix, set with C<set_suffix>.

=back

=back

=cut

1;

=head1 REVISION HISTORY

=over 4

=item Version 0.12 (2002-01-06)

First public beta. Changed constructor to expect hash to be passed by 
+reference. Split C<_init> out from C<new>.

=item Version 0.11 (2002-01-05)

Removed method caching (which caused conflicts when instantiating mult
+iple objects), rationalized code,
completed POD.

=item Version 0.10 (2001-12-17)

Initial implementation.

=back

=head1 AUTHOR

John Clyman (module-support@clyman.com)

=head1 COPYRIGHT AND LICENSE

Copyright (c) 2002 John Clyman.

Permission is hereby granted, free of charge, to any person obtaining 
+a copy of this software and associated documentation files (the "Soft
+ware"), to deal in the Software without restriction, including withou
+t limitation the rights to use, copy, modify, merge, publish, distrib
+ute, sublicense, and/or sell copies of the Software, and to permit pe
+rsons to whom the Software is furnished to do so, subject to the foll
+owing conditions:

The above copyright notice and this permission notice shall be include
+d in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRES
+S OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANT
+ABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 
+EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT O
+R OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 
+OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

=cut