How discoverable and usable is your PERL module?

I am working on using the IMAPI2 COM component in one of our products for an improved data burning experience. After exhaustively searching for anyone's C# port of the COM error codes laid out by Microsoft, I came up dry. There is a C++ header file included with the Windows SDK, but that's doesn't help me in C# where I wanted to use an enum (uint).

There are 85+ error codes and I also wanted to easily link them to resource file descriptions of the error for localization purposes and it would mean a very tedious import of each enum as the lookup for the string, coupled with 85+ copy/pastes. Sound like a job for PERL? Well, I agree. I haven't used PERL for several months and saw this as an opportunity to pull out my Swiss Army Knife of programming.


I am posting this to show how little code there is, and yet how long it took me to get it working (took my about 3 1/2 hours to get the script to run perfectly). Does 3 1/2 hours sound like an incredibly long amount of time to 'automate' some copy/pasting? I agree, but I'm...retentive about tasks that I undertake (as a friend would say).

I don't like any programming that takes an input and changes the output just b/c it wants to unless you explicitly override it. That's what XML::Simple was doing. For all you PERL programmers, you know this is a common practice in a lot of modules (at least ones I've used). There have been far too many times that I've had to dig into a module to figure out how to get it work the way I want it to, and I've often had to fix modules with bugs that are posted on CPAN. I know that I can use them only with the understanding that they are not bug free and at my own risk, but it's still very annoying and (I think) easily fixable.

My opinion (formed over 3 years of using PERL almost on a daily basis) is that you can program PERL in a highly modular, object oriented, and loosely coupled fashion, but it is REALLY difficult to do that effectively. I'm quickly becoming of the opinion that if you are a really good PERL programmer and that PERL is your language of choice, then you are probably not good at programming in C++, C# or any other OO language. However, a developer can develop a module so that it exposes functions to be consumed that do exactly what they say they will do, and nothing else. Do not ever make an assumption about what a consumer wants when it calls your functions!! Doing this will greatly increase the usability and discoverability of your module.

Most programmers I know who use PERL effectively see it as a 'quick hack' language, which should be perfectly legit for the consumer of the various CPAN modules who want to create a quick script to do some tedious work, but the modules themselves should be more of a black box with easily discoverable behaviors and capabilities. Some of them are very good at this, like the GetOptLong module or the Carp module (one of my favorites for tracing), but others are not and take hours to learn how to utilize fully. I would probably have been better off to write my own module rather than waste time with using someone else's, and the owner(s) of those modules would probably agree. :)

Problem and Root Cause:

The problem with XML::Simple was that it was taking my hash and parenting it with an opt level and it wasn't putting an argument on a child node like I wanted. This (after looking through the module; the help comments did help a bit, but not much) was fixed by defining the RootName attribute to an empty string and by putting the value of the child node in an array type.

The fixes for my problem were not readily discoverable and required quite a bit of research on my part to get them. The point of good code is to reduce complexity, and having the end user become an expert in your module before he can use it INCREASES complexity and stress. I don't mean to harp on the author of XML::Simple so much as convince people that:
  • This is typical behavior for many PERL modules.
  • The PERL community at large thinks this is OK, or it wouldn't be this way.
  • This needs to change!

So here's the hacked code I came up with to do the job. I would like to believe that it is reusable with some slight modifications, but who knows? Feel free to use/modify it:
use File::Slurp;
use Carp;
use Getopt::Long;
use XML::Simple qw(:strict);
use Data::Dumper;
use strict;
 
my %string_entries = ();
 
# should look like:
# %string_entries = (
   # E_IMAPI_REQUEST_CANCELLED => { value = The request was cancelled. },
# );
 
my $sourceFile = '\Imapi2Interop.cs';
my $designerFile = '\IMAPI2ErrorStrings.Designer.cs';
my $resxFile = '\IMAPI2ErrorStrings.resx';
 
my @lines = read_file($sourceFile);
 
# Find all the enum entries
my @filtered_lines;
my $begin_capture = 0;
my $stop_capture = 0;
foreach my $line (@lines) {
 if($line =~ /public enum IMAPIExceptionCodes : uint {/) {
  $begin_capture = 1; 
  next;
 }

 if($line =~ /}/ and $begin_capture) {
  last;
 }

 if($begin_capture and !$stop_capture and $line =~ /\/|\w/) {
  push(@filtered_lines, $line);
 }
}
 
#print Dumper(@filtered_lines);
 
# Split the enum entry into 2 components: comment in  and the enum name
# store it into a hash to be used for XML output
$begin_capture = 0;
my @texts;
my $numEntries = 0;
foreach my $fline (@filtered_lines) {
 chomp($fline);
 if($fline =~ //) {
  $begin_capture = 1;
  next;
 }

 # Found the key
 if($fline !~ /\// and $begin_capture) {
  my ($key, undef) = split(' =', $fline);
  $key =~ s/\s+?//g;
  $string_entries{$numEntries}{'data'} = { 'name' => $key, 'xml:space' => 'preserve', 'value' => [join("\n", @texts)]};
  @texts = ();
  $begin_capture = 0;
  $numEntries++;
  next;
 }

 # Found (part of) the value
 if($begin_capture) {
  my (undef, $text) = split('/// ', $fline);
  if($text !~ /summary/) {
   $text =~ s/^\s+?//g;
   chomp($text);
   push(@texts, $text);
  }
 }
}
 
#append the XML entries to the .resx
# 
# The request was cancelled.
# 
my $xs = XML::Simple->new(Attrindent => 1, KeyAttr => { 'junk' => "name" }, RootName => '');
print Dumper($xs->XMLout(\%string_entries));
append_file($resxFile, "\n");
foreach my $item (keys (%string_entries)) {
 append_file($resxFile, $xs->XMLout($string_entries{$item}));
 #print Dumper($xs->XMLout($string_entries{$item}));
}
 
#create a method in the designer file
# /// 
# ///   Looks up a localized string similar to The request was cancelled..
# /// 
# internal static string E_IMAPI_REQUEST_CANCELLED {
# get {
 # return ResourceManager.GetString("E_IMAPI_REQUEST_CANCELLED", resourceCulture);
# }
# }
my @appendDes = ();
foreach my $item (keys(%string_entries)) {
push(@appendDes, "\n\n        /// 
 ///   Looks up a localized string similar to ".join('\n', @{$string_entries{$item}{'data'}{'value'}})."
 /// 
 internal static string $string_entries{$item}{'data'}{'name'} {
  get {
   return ResourceManager.GetString(\"$string_entries{$item}{'data'}{'name'}\", resourceCulture);
  }
 }");
}

 
append_file($designerFile, @appendDes);

Comments

Popular posts from this blog

35x Improved T-SQL LevenShtein Distance Algorithm...at a cost

The hidden workings of __doPostBack - Full or Partial Postback?

Facing Death...dum de dum dum