Beefy Boxes and Bandwidth Generously Provided by pair Networks
P is for Practical
 
PerlMonks  

MessageLibrary

by seattlejohn (Deacon)
on Jan 07, 2002 at 09:24 UTC ( [id://136784]=sourcecode: print w/replies, xml ) Need Help??
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
Replies are listed 'Best First'.
Re: MessageLibrary
by mkmcconn (Chaplain) on Jan 07, 2002 at 11:42 UTC

    I like your MessageLibrary, seattlejohn. It works as advertised, and I think it would be quite useful for a larger project.

    In the humble hope of offering something more useful to you than an 'attaboy': You put a lot of work into the pod, and evidently want it to be as informative as possible; still, I found the inline pod very difficult to read around while trying to see what the code was doing (until I loaded the source in a highlighting editor).

    Also, it might be helpful if your comments on how to use it were even more clearly distinguished from what's happening inside the program (for example, I'm thinking here of the pod comments on Private Methods and variables).

    Thank you for sharing your work, here.
    mkmcconn

      Thanks for the feedback, mkmcconn. Your comment about the inline pod doesn't come as a complete surprise; it occurred to me before posting that this could be seen as an impediment to following the code flow. I must admit that I routinely use a highlighting editor and kind of like having the documentation right next to the code it applies to, partly because I find it's easier to keep things in sync that way. But given your comment (and my suspicion that for everyone who comments there are probably ten who feel the same way but can't be bothered to respond), I'll definitely reconsider. cheers

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: sourcecode [id://136784]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others scrutinizing the Monastery: (8)
As of 2024-04-23 16:38 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found