initial commit of re-written stem2pod
Steve Scaffidi [Thu, 25 Jun 2009 07:26:30 +0000 (03:26 -0400)]
bin/stem2pod

index 97143d5..6e070af 100755 (executable)
-#!/usr/local/bin/perl -w
+#!/usr/bin/perl -w
 #
-#      stem2pod
+#   stem2pod
 #
-#      takes filename (a stem module) arguments and it updates their
-#      pod from their attribute descriptions. it also will insert pod
-#      templates for methods, subs and standard pod sections.
+# scans a file containing the code for a stem cell
+# and updates it with pod generated from the cell's
+# attribute variables.
+#
+# also generates pod for detected methods, subs, and
+# any missing standard stuff (author, license, etc.)
 #
-#      if a file is changed, it is written out over itself. unchanged
-#      files are not touched.
 
 use strict;
-
-use Carp qw( carp cluck ) ;
+use warnings;
 use Data::Dumper;
+use File::Slurp;
+use Carp;
+
 
-#use Test::More tests => 1 ;
 
-#$SIG{__WARN__} = sub { cluck } ;
+# These globals are initalized in the BEGIN block at the bottom of this file
+# Please treat as constants.
+our ( %PRE_POD, %POST_POD );
 
-my $changed ;
-my $package ;
+# Another global/"constant"
+our %IS_ATTR_PART = map { $_ => 1 } qw(
+  name
+  type
+  help
+  default
+  required
+  class
+  class_args
+);
 
-my %is_attr_part = map { $_ => 1 } qw(
-       name
-       type
-       help
-       default
-       required
-       class
-       class_args
-) ;
 
-foreach my $file_name ( @ARGV ) {
 
-       process_source_file( $file_name ) ;
+for my $file_name (@ARGV) {
+    stem2pod($file_name);
 }
+exit;
 
-exit ;
 
-sub process_source_file {
 
-       my ( $file_name ) = @_ ;
 
-       my $code_text = read_file( $file_name ) ;
+sub stem2pod {
+    my ($file_name) = @_;
 
-       my $new_code_text = process_code_text( $file_name, $code_text ) ;
+    my $file_text = read_file($file_name);
 
-#print $new_code_text ;
+    my $attr_spec = find_attr_spec( $file_text, $file_name );
 
-       if ( $new_code_text eq $code_text ) {
+    my @meth_names = find_method_names( $file_text );
 
-               print "$file_name SAME\n" ;
-               return ;
-       }
+    # extract_pod _literally_ extracts it - 
+    # removing any pod found from the text.
+    my $new_text = $file_text;
+    my $pod_sections = extract_pod( \$new_text, \@meth_names );
 
-       print "$file_name CHANGED\n" ;
+    my ($class_name) = ($file_text =~ /^package\s+([\w:]+)/m);
 
-       write_file( "$file_name.new, $new_code_text ) ;
+    my $attr_pod = generate_attr_pod( $attr_spec, $file_name, $class_name );
+    
+    generate_boilerplate( $file_text, $class_name );
 
-#      write_file( "$file_name.bak, $code_text ) ;
-#      write_file( $file_name, $new_code_text ) ;
+    my $new_pod = generate_pod( $attr_pod, \@meth_names, $pod_sections );
 
-}
+    my $new_source = build_source( $new_pod, $new_text );
+
+    if ( $file_text eq $new_source ) {
+        warn "No changes to make to $file_name\n";
+        return;
+    }
 
-sub process_code_text {
-
-       my ( $file_name, $text ) = @_ ;
-
-       $text =~ s{
-                 (
-                    ^package                   # start at package line
-                    .+?                        # the middle stuff
-                    ^sub                       # start of constructor
-                 )
-               }
-               {
-                 update_attr_spec( $1, $file_name )
-               }mgsex ;
-
-       $text =~ s{
-                       (.{0,20}?)
-                       ^sub
-                       \s+
-                       (\w+)
-                       \s*
-               }
-               { update_sub_pod( $1, $2 ) }mgsex ;
-
-       unless( $text =~ /^=cut\s*^\s*1\s*;\s*/m ) {
-       
-               $text =~ s{^\s*1\s*;\s*$}{ update_trailing_pod() }mex ;
-       }
-
-       return $text ;
+    write_file( "$file_name.new", $new_source );
 }
 
 
-sub update_attr_spec {
 
-       my( $attr_text, $file_name ) = @_ ;
 
-#print "U1 <$attr_text>\n" ;
 
-       ( $package ) = $attr_text =~ /^package\s+([\w:]+)/ ;
 
-       $attr_text =~ s/\n*^\#{5,}\n.+?^\#{5,}\n*//ms ;
-# and print "DELETED OLD POD\n" ;
 
-#print "U3 <$attr_text>\n" ;
 
-       $attr_text =~ s{ (^my\s+\$attr_spec.+?^]\s*;\s*) }
-                      { attr_spec_to_pod( $1, $file_name ) }gmsex ;
 
-#dump_attr( 'ATTR', $attr_text ) ;
-#print "ATTR [", substr( $attr_text, -40 ), "]\n" ;
-#print "U2 [$attr_text]\n" ;
 
-       return $attr_text ;
-}
 
-sub attr_spec_to_pod {
 
-       my ( $attr_text, $file_name ) = @_ ;
 
-       my $pod ;
 
-#print "ATTR [$attr_text]\n" ;
-#print "ATTR END1 [", substr( $attr_text, -30), "]\n" ;
+# Finds and returns a list of the names of 
+# methods/subroutines found in the file
+sub find_method_names {
+    my ( $file_text ) = @_;
 
-       $attr_text =~ s/\s*\z// ;
+    my @meth_names = $file_text =~ /^sub\s+([^\W_]\w*)/gm;
 
-       my( $attr_list_text ) =
-               $attr_text =~ /^my\s+\$attr_spec.+?=(.+?^\])/ms ;
-       $attr_list_text or die
-             "can't parse out attr list from file $file_name class $package" ;
+    return @meth_names;
+}
 
-#print "ATTR2 [$attr_list_text]\n" ;
-       my $attr_list = eval $attr_list_text ;
 
-       $pod .= <<POD ;
-###########
-# This POD section is autogenerated. Any edits to it will be lost.
 
-=head2 Class Attributes for $package
 
-=over 4
 
-POD
+# Find and _remove_ any POD in the file. 
+# Returns a HoH-like data-structure that you should probably inspect
+# with DD to see what it looks like...
+sub extract_pod {
+    my ( $text_ref, $method_names ) = @_;
+    my %pod_sections;
+    my @pod_names;
+    while ( ${$text_ref} =~ 
+        s{
+               ^                # begin of line
+               (                # grab pod head line as $1
+                   =head\w* # begin pod section
+                       \s+
+                       (.+?)$   # grab section title as $2
+            )
+            (
+                       .+?      # grab pod body as $3
+               )
+               (?:          # match until after =cut, before =head or at EOF
+                ^=cut
+              | (?=^=head)
+              | \z
+            )
+        }{}mxs 
+    ) {
+        my $head  = $1;
+        my $title = $2;
+        my $body  = $3;
+
+#        # strip out leading and trailing blank lines
+#        $body =~ s/\A[\n]\s*[\n]|[\n]\s*[\n]\z//msg;
+
+        # Does the section title contain the name of a method?
+        my ($method_name) = grep { $title =~ /$_/i and $_ } @$method_names;
+        if ( $method_name ) {
+        
+            warn "Duplicate pod section for method "
+               . "$method_name named [$title].\n"
+              if exists $pod_sections{methods}{$method_name};
+              
+            $pod_sections{methods}{$method_name}{head} = $head;  
+            $pod_sections{methods}{$method_name}{body} = $body;
+            
+            next;
+        }
+        
+        warn "Found pod section with duplicate name [$title]\n"
+          if exists $pod_sections{other}{ lc $title };
+          
+        $pod_sections{other}{ lc $title }{head} = $head;
+        $pod_sections{other}{ lc $title }{body} = $body;
+    }
+#    print Dumper \%pod_sections; exit;
+    return \%pod_sections;
+}
 
-#print "POD [$pod]\n" ;
 
 
-       foreach my $attr_ref ( @{$attr_list} ) {
 
-               my $name = $attr_ref->{name} ;
 
-               if ( $name ) {
 
-                       $pod .= <<POD ;
+# Generate all the POD text in one big chunk...
+# Fill in most sections with boilerplate, but will
+# merge in sections that were already present ( found by extract_pod() )
+sub generate_pod {
+    my ( $attr_pod, $meth_names, $pod_sections ) = @_;
 
-=item * Attribute - B<$name>
+    my $new_pod;
 
-=over 4
+    $new_pod .= generate_pod_sections( $pod_sections, \%PRE_POD );
 
-POD
-               }
-               else {
+    # attr_pod is always regenerated...
+    # TODO: perhaps it's better to call generate_attr_pod() here?
+    if ($attr_pod) {
+        $new_pod .= "=head1 ATTRIBUTES\n\n";
+        $new_pod .= strip_blank_lines( $attr_pod ) . "\n\n";
+    }
 
-                       warn <<WARN ;
-Missing attribute name in Class $package in file $file_name
-WARN
+    $new_pod .= "=head1 METHODS\n\n";
+    foreach my $meth_name ( @$meth_names ) {
 
-                       next ;
-               }
+        $new_pod .= "=head2 $meth_name\n\n";
+        if ( my $meth_pod = $pod_sections->{methods}{$meth_name} ) {
+            $new_pod .= strip_blank_lines( $meth_pod->{body} ) . "\n\n";
+            next;
+        }
 
-               my $help = $attr_ref->{help} ;
+        if ( $meth_name eq 'new' ) {
+            $new_pod .= strip_blank_lines( <<END_POD ) . "\n\n";
 
-               if ( defined( $help ) ) {
+This is a class method that constructs the object. It uses
+the standard Stem API of key/value attributes. These are described
+in the \$attr_spec in the module, and it's POD is auto-generated.
 
-                       $pod .= <<POD ;
+END_POD
+            next;
+        }
 
-=item Description:
+        if ( $meth_name eq 'msg_in' ) {
+            $new_pod .= strip_blank_lines( <<END_POD ) . "\n\n";
 
-$help
-POD
-               }
-               else {
+This method is the 'wildcard' Stem message handler and it is passed a single 
+argument which is a L<Stem::Msg> object. This method is called if no 
+specific message handler method is found in this cell. This is missing
+specific documentation to be filled in.
 
-                       warn <<WARN ;
-Missing help in attribute $name in Class $package in file $file_name
-WARN
-               }
+END_POD
+            next;
+        }
 
-               if ( my $attr_class = $attr_ref->{class} ) {
+        if ( $meth_name =~ /^(.+)_in$/ ) {
+            my $msg_type = $1; 
+            $new_pod .= strip_blank_lines( <<END_POD ) . "\n\n";
 
-                       my $class_args = '<' .
-                               join( ', ', @{$attr_ref->{class_args} || []} )
-                                . '>' ;
+This method is a Stem message handler for messages of type $msg_type.
+It is passed a single argument which is a L<Stem::Msg> object and 
+its return value is ignored. This is missing specific documentation 
+to be filled in.
 
-                       $pod .= <<POD ;
+END_POD
+            next;
+        }
 
-=item Class Attribute:
+        if ( $meth_name =~ /^(.+)_cmd$/ ) {
+            my $cmd_type = $1;
+            $new_pod .= strip_blank_lines( <<END_POD ) . "\n\n";
 
-'$name' is an object of class $attr_class and constructed with:
-$class_args
-POD
-               }
+This method is a Stem message handler for command messages of type $cmd_type.
+It is passed a single argument which is a L<Stem::Msg> object. 
+Any return value will be sent back as a 'response' type message to the sender.
+This is missing specific documentation to be filled in.
 
+END_POD
+            next;
+        }
 
-               exists( $attr_ref->{type} ) and $pod .= <<POD ;
+        # All other methods just get this...
+        $new_pod .= "This method is missing documentation\n\n";
+    }
 
-=item The type of '$name' is:
+    $new_pod .= generate_pod_sections( $pod_sections, \%POST_POD );
+    
+    $new_pod .= "=cut\n\n";
 
-$attr_ref->{type}
-POD
+    return $new_pod;
+}
 
-               if ( exists( $attr_ref->{default} ) ) {
 
-                       my $default = $attr_ref->{default} ;
+sub generate_pod_sections {
+    my ($existing_pod, $bp_pod) = @_;
+    
+    my $new_pod;
 
-                       if( ref($default) eq "ARRAY" ) {
+    foreach my $section ( @{ $bp_pod->{names} } ) {
 
-                               $default =
-                                       '(' . join( ', ', @{$default} ) . ')' ;
-                       }
+        (my $printable_section = $section) =~ tr/_/ /;
+        $new_pod .= "=head1 $printable_section\n\n";
 
-                       $pod .= <<POD
+        my $found_sec = $existing_pod->{other}{ lc $section };
+        
+        my $next_part = $found_sec ? 
+            $found_sec->{body} : $bp_pod->{boilerplate}{$section} ;
 
-=item B<Default> value:
+        $new_pod .= strip_blank_lines( $next_part ) . "\n\n";
+    }
 
-$default
-POD
-               }
+    return $new_pod;
+}
 
-               exists( $attr_ref->{required} ) and $pod .= <<POD ;
 
-=item It is B<required>.
-POD
+sub generate_boilerplate {
+    my ($file_text, $class_name) = @_;
 
-               foreach my $attr ( sort keys %{ $attr_ref } ) {
-                       next if $is_attr_part{ $attr } ;
-                       $pod .= "Unknown attribute $attr\n" ;
-               }
+    $PRE_POD{boilerplate}{NAME} = <<END_POD;
+$class_name
+END_POD
 
-               $pod .= <<POD ;
+    if ($file_text =~ /\$attr_spec/) {
+        $POST_POD{boilerplate}{SEE_ALSO} = <<END_POD;
+L<Stem::Class>
+END_POD
+    }
 
-=back
+return;
+}
 
-POD
-       }
+# strip out leading and trailing blank lines
+sub strip_blank_lines {
+    my ($text) = @_;
+    $text =~ s/\A[\n]?\s*[\n]|[\n]\s*[\n]?\z//msg;
+    return $text;
+}
 
-       $pod .= <<POD ;
 
-=back
+# tries to locate the $attr_spec array ref in the text
+# and returns it as a real data structure (eval magick)
+sub find_attr_spec {
+    my ( $file_text, $file_name ) = @_;
 
-=cut
+    # The regex below is naive but will be fine for now. I would
+    # prefer to use Text::Balanced or Regexp::Common though...
+    my $attr_spec;
+    if ( $file_text =~ /\$attr_spec\s*=\s*(\[[^\]]*\])\s*;/ ) {
+        $attr_spec = eval $1
+          or croak "Error parsing \$attr_spec from $file_name:\n$@\n";
+    }
 
-# End of autogenerated POD
-###########
+    return $attr_spec;
+}
 
-POD
 
-#print "[$pod]" ;
-#print "POD2 [", substr($pod, 0, 40), "]\n" ;
 
-       return "$attr_text\n\n$pod" ;
+
+# blow away __END__ and everything after (if present) 
+# and replace with generated POD
+sub build_source {
+    my ( $new_pod, $file_text ) = @_;
+
+    ( my $new_text = $file_text ) =~ s/^__END__[\n].*//msg;
+
+    return $new_text . "__END__\n\n" . $new_pod;
 }
 
-sub update_sub_pod {
 
-       my( $cut_text, $name ) = @_ ;
 
-#print "SUB [$cut_text][$name]\n" ;
 
-       if ( $cut_text =~ /^=cut\s*$/m || $name =~ /^_/ ) {
 
-#print "SUB1 [${cut_text}sub $name ]\n"  if $name eq 'new' ;
-#dump_new( 'POD FOUND', $cut_text ) ;
 
-               return "${cut_text}sub $name " ;
-       }
 
-#print "NO SUB POD for $name\n" ;
 
-       my $desc = get_sub_pod( $name ) ;
+# Stolen from uri's original script, but really, 
+# really needs to be cleaned up...
+# TODO: clean up this sub, somehow...
+sub generate_attr_pod {
+    my ( $attr_list, $file_name, $package ) = @_;
 
-#dump_new( 'CUT', $cut_text ) ;
-#dump_new( 'DESC', $desc ) ;
-#print "CUT2 [$cut_text]\nDESC [$desc]\n" if $name eq 'new' ;
+    my $pod .= <<POD ;
 
-       my $pod = <<POD ;
-$cut_text$desc
-=cut
+=head2 Class Attributes for $package
+
+This pod is generated from the \$attr_spec in the module.
+That data is also used by L<Stem::Class> to parse the arguments 
+passed to the class constructor new().
+
+=over 4
 
-sub $name 
 POD
 
-       chomp $pod ;
+    #print "POD [$pod]\n" ;
 
-#print "SUB2 [$pod]\n" if $name eq 'new' ;
+    foreach my $attr_ref ( @{$attr_list} ) {
 
-       return $pod ;
-}
+        my $name = $attr_ref->{name};
+
+        unless ($name) {
 
-sub get_sub_pod {
+            warn <<WARN ;
+Missing attribute name in Class $package in file $file_name
+WARN
+            next;
+        }
 
-       my ( $name ) = @_ ;
+        $pod .= <<POD ;
 
-       return <<POD if $name eq 'new' ;
-=head3 Constructor - B<new>
+=item * B<$name>
 
-The B<new> method creates an object of the class B<$package>. 
+=over 4
 
 POD
 
-       return <<POD if $name eq 'msg_in' ;
-=head3 Message Handler - B<msg_in>
+        my $help = $attr_ref->{help};
 
-The B<msg_in> method is effectively a default method for message
-delivery. If any message to this cell can't be delivered to another
-method, then it will be delivered to the B<msg_in> method. If a
-command message is delivered and a value is returned by B<msg_in>, a
-response message is sent back to the originating cell with that value.
-POD
+        unless ( defined($help) ) {
+            warn <<WARN ;
+Missing help in attribute $name in Class $package in file $file_name
+WARN
+            next;
+        }
 
-       return <<POD if $name =~ /(\w+)_in$/ ;
-=head3 Message Handler - $name
+        $pod .= <<POD ;
 
-B<$1> type messages are delivered to this method. Its return value is
-ignored by the message delivery system.
+=item - $help
 POD
+# suppress line break in POD output after description
+        chomp $pod; 
+
+        if ( my $attr_class = $attr_ref->{class} ) {
 
-       return <<POD if $name =~ /(\w+)_cmd$/ ;
-=head3 Command Message Handler - $name
+            my $class_args =
+              '<' . join( ', ', @{ $attr_ref->{class_args} || [] } ) . '>';
 
-B<$1> command messages are delivered to this method. If any value is
-returned, the message delivery system will create a response type
-message and dispatch it back to the sending cell.
+            $pod .= <<POD ;
+=item - Class Attribute: '$name' is an object of class $attr_class and constructed with: $class_args
 POD
+        }
 
-       return <<POD ;
-=head3 Method - $name
+        exists( $attr_ref->{type} ) and $pod .= <<POD ;
+
+=item - The type of '$name' is: $attr_ref->{type}
 POD
 
-}
+        if ( exists( $attr_ref->{default} ) ) {
 
-sub update_trailing_pod {
+            my $default = $attr_ref->{default};
 
-       my( $tail_text ) = @_ ;
+            if ( ref($default) eq "ARRAY" ) {
 
-#      return $tail_text if $tail_text =~ /=cut/ ;
+                $default = '(' . join( ', ', @{$default} ) . ')';
+            }
 
-#print "1 [$tail_text]\n" ;
+            $pod .= <<POD
 
-       return <<POD ;
+=item - B<Default> value: $default
+POD
+        }
 
-=head1 Bugs
+        exists( $attr_ref->{required} ) and $pod .= <<POD ;
 
-=head1 Todo
+=item - It is B<required>.
+POD
 
-=head1 See Also
+        foreach my $attr ( sort keys %{$attr_ref} ) {
+            next if $IS_ATTR_PART{$attr};
+            $pod .= "Unknown attribute $attr\n";
+        }
 
-=head1 Author
+        $pod .= <<POD ;
+     
+=back 
+  
+POD
+    }
 
-Uri Guttman, E<lt>uri\@stemsystems.comE<gt>
+    $pod .= <<POD ;
+
+=back
 
 =cut
 
-1 ;
 POD
 
+    #print "[$pod]" ;
+
+    #print "POD2 [", substr($pod, 0, 40), "]\n" ;
+
+    return $pod;
 }
 
-sub read_file {
 
-       my( $file_name ) = shift ;
 
-       local( *FH ) ;
-       open( FH, $file_name ) || carp "can't open $file_name $!" ;
 
-       return <FH> if wantarray ;
 
-       my $buf ;
 
-       sysread( FH, $buf, -s FH ) ;
-       return $buf ;
-}
 
-sub write_file {
 
-       my( $file_name ) = shift ;
 
-       local( *FH ) ;
 
-       open( FH, ">$file_name" ) || carp "can't create $file_name $!" ;
 
-       print FH @_ ;
-}
+# Globals declared at the top of this script are initalized here
+# Keeping at the bottom of this file to (try to) reduce clutter.
+BEGIN {
+
+    # Pod sections that go before attribute and method sections
+    my @PRE_POD_NAMES = qw( NAME SYNOPSIS DESCRIPTION );
+
+    # sections that go after
+    my @POST_POD_NAMES = qw( BUGS TODO SEE_ALSO AUTHOR LICENSE COPYRIGHT );
+
+
+    # Boilerplate for each "pre" section
+    my %PRE_POD_BP = (
+
+        NAME => <<'END_POD',
+Name of module
+END_POD
+
+        SYNOPSIS => <<'END_POD',
+Code/config example
+END_POD
+
+        DESCRIPTION => <<'END_POD',
+What is this module good for?
+END_POD
+
+    );
+
+
+    # Boilerplate for each "post" section
+    my %POST_POD_BP = (
+
+        BUGS => <<'END_POD',
+None
+END_POD
+
+        TODO => <<'END_POD',
+END_POD
+
+        SEE_ALSO => <<'END_POD',
+END_POD
+
+        AUTHOR => <<'END_POD',
+Uri Guttman uri@cpan.org
+END_POD
+
+        LICENSE => <<'END_POD',
+Same as Perl
+END_POD
+
+        COPYRIGHT => <<'END_POD',
+(C) Uri Guttman, 2009
+END_POD
+
+    );
 
-sub dump_attr {
 
-       my( $key, $text ) = @_ ;
+    # globals used in other parts of the program
+    %PRE_POD = (
+        names       => \@PRE_POD_NAMES,
+        boilerplate => \%PRE_POD_BP,
+    );
 
-       $text =~ /(;\s+#{3,})/s or return ;
+    %POST_POD = (
+        names       => \@POST_POD_NAMES,
+        boilerplate => \%POST_POD_BP,
+    );
 
-       print "$key [$1]\n" ;
 }
 
-__END__