File Coverage

blib/lib/App/podweaver.pm
Criterion Covered Total %
statement 46 48 95.8
branch n/a
condition n/a
subroutine 16 16 100.0
pod n/a
total 62 64 96.8


line stmt bran cond sub pod time code
1             package App::podweaver;
2              
3             # ABSTRACT: Run Pod::Weaver on the files within a distribution.
4              
5 1     1   24446 use warnings;
  1         3  
  1         33  
6 1     1   6 use strict;
  1         2  
  1         33  
7              
8 1     1   5 use Carp;
  1         6  
  1         70  
9 1     1   860 use Config::Tiny;
  1         1077  
  1         33  
10 1     1   1238 use CPAN::Meta;
  1         58279  
  1         31  
11 1     1   1081 use IO::File;
  1         9714  
  1         146  
12 1     1   939 use File::Copy;
  1         4793  
  1         64  
13 1     1   1520 use File::HomeDir;
  1         6410  
  1         62  
14 1     1   843 use File::Find::Rule;
  1         7954  
  1         7  
15 1     1   889 use File::Find::Rule::Perl;
  1         4964  
  1         11  
16 1     1   913 use File::Find::Rule::VCS;
  1         1135  
  1         10  
17 1     1   820 use File::Slurp ();
  1         14940  
  1         22  
18 1     1   10 use File::Spec;
  1         2  
  1         21  
19 1     1   781 use Log::Any qw/$log/;
  1         2266  
  1         4  
20 1     1   1064 use Module::Metadata;
  1         5545  
  1         39  
21 1     1   427 use Pod::Elemental;
  0            
  0            
22             use Pod::Elemental::Transformer::Pod5;
23             use Pod::Weaver;
24             use PPI::Document;
25             use Try::Tiny;
26              
27             our $VERSION = '1.00';
28              
29             sub FAIL() { 0; }
30             sub SUCCESS_UNCHANGED() { 1; }
31             sub SUCCESS_CHANGED() { 2; }
32              
33             sub weave_file
34             {
35             my ( $self, %input ) = @_;
36             my ( $file, $no_backup, $write_to_dot_new, $weaver );
37             my ( $perl, $ppi_document, $pod_after_end, @pod_tokens, $pod_str,
38             $pod_document, %weave_args, $new_pod, $end, $new_perl,
39             $output_file, $backup_file, $fh, $module_info );
40              
41             unless( $file = delete $input{ filename } )
42             {
43             $log->errorf( 'Missing file parameter in args %s', \%input )
44             if $log->is_error();
45             return( FAIL );
46             }
47             unless( $weaver = delete $input{ weaver } )
48             {
49             $log->errorf( 'Missing weaver parameter in args %s', \%input )
50             if $log->is_error();
51             return( FAIL );
52             }
53             $no_backup = delete $input{ no_backup };
54             $write_to_dot_new = delete $input{ new };
55              
56             # From here and below is mostly hacked out from
57             # Dist::Zilla::Plugin::PodWeaver
58              
59             $perl = File::Slurp::read_file( $file );
60              
61             unless( $ppi_document = PPI::Document->new( \$perl ) )
62             {
63             $log->errorf( "PPI error in '%s': %s", $file, PPI::Document->errstr() )
64             if $log->is_error();
65             return( FAIL );
66             }
67              
68             # If they have some pod after __END__ then assume it's safe to put
69             # it all there.
70             $pod_after_end =
71             ( $ppi_document->find( 'PPI::Statement::End' ) and
72             grep { $_->find_first( 'PPI::Token::Pod' ) }
73             @{$ppi_document->find( 'PPI::Statement::End' )} ) ?
74             1 : 0;
75              
76             @pod_tokens =
77             map { "$_" } @{ $ppi_document->find( 'PPI::Token::Pod' ) || [] };
78             $ppi_document->prune( 'PPI::Token::Pod' );
79              
80             if( $ppi_document->serialize =~ /^=[a-z]/m )
81             {
82             # TODO: no idea what the problem is here, but DZP::PodWeaver had it...
83             $log->errorf( "Can't do podweave on '%s': " .
84             "there is POD inside string literals", $file )
85             if $log->is_error();
86             return( FAIL );
87             }
88              
89             $pod_str = join "\n", @pod_tokens;
90             $pod_document = Pod::Elemental->read_string( $pod_str );
91              
92             # TODO: This _really_ doesn't like being run twice on a document with
93             # TODO: regions for some reason. Comment out for now and trust they
94             # TODO: have [@CorePrep] enabled.
95             # Pod::Elemental::Transformer::Pod5->new->transform_node( $pod_document );
96              
97             %weave_args = (
98             %input,
99             pod_document => $pod_document,
100             ppi_document => $ppi_document,
101             filename => $file,
102             );
103              
104             $module_info = Module::Metadata->new_from_file( $file );
105             if( $module_info and defined( $module_info->version() ) )
106             {
107             $weave_args{ version } = $module_info->version();
108             }
109             elsif( defined( $input{ dist_version } ) )
110             {
111             $log->warningf( "Unable to parse version in '%s', " .
112             "using dist_version '%s'", $file, $input{ dist_version } )
113             if $log->is_warning();
114             $weave_args{ version } = $input{ dist_version };
115             }
116             else
117             {
118             $log->warningf( "Unable to parse version in '%s' and " .
119             "no dist_version supplied", $file )
120             if $log->is_warning();
121             }
122              
123             # Try::Tiny this, it can croak.
124             try
125             {
126             $pod_document = $weaver->weave_document( \%weave_args );
127              
128             $log->errorf( "weave_document() failed on '%s': No Pod generated",
129             $file )
130             if $log->is_error() and not $pod_document;
131             }
132             catch
133             {
134             $log->errorf( "weave_document() failed on '%s': %s",
135             $file, $_ )
136             if $log->is_error();
137             $pod_document = undef;
138             };
139             return( FAIL ) unless $pod_document;
140              
141             $new_pod = $pod_document->as_pod_string;
142              
143             $end = do {
144             my $end_elem = $ppi_document->find( 'PPI::Statement::Data' )
145             || $ppi_document->find( 'PPI::Statement::End' );
146             join q{}, @{ $end_elem || [] };
147             };
148              
149             $ppi_document->prune( 'PPI::Statement::End' );
150             $ppi_document->prune( 'PPI::Statement::Data' );
151              
152             $new_perl = $ppi_document->serialize;
153              
154             $new_perl =~ s/\n+$//;
155             $new_perl .= "\n";
156              
157             $new_pod =~ s/\n+$//;
158             $new_pod =~ s/^\n+//;
159             $new_pod .= "\n";
160              
161             if( not $end )
162             {
163             $end = "__END__\n\n";
164             $pod_after_end = 1;
165             }
166              
167             if( $pod_after_end )
168             {
169             $new_perl = "$new_perl\n$end$new_pod";
170             }
171             else
172             {
173             $new_perl = "$new_perl\n$new_pod\n$end";
174             }
175              
176             if( $perl eq $new_perl )
177             {
178             $log->infof( "Contents of '%s' unchanged", $file )
179             if $log->is_info();
180             return( SUCCESS_UNCHANGED );
181             }
182              
183             $output_file = $write_to_dot_new ? ( $file . '.new' ) : $file;
184             $backup_file = $file . '.bak';
185              
186             unless( $write_to_dot_new or $no_backup )
187             {
188             unlink( $backup_file );
189             copy( $file, $backup_file );
190             }
191              
192             $log->debugf( "Writing new '%s' for '%s'", $output_file, $file )
193             if $log->is_debug();
194             # We want to preserve permissions and other stuff, so we open
195             # it for read/write.
196             $fh = IO::File->new( $output_file, $write_to_dot_new ? '>' : '+<' );
197             unless( $fh )
198             {
199             $log->errorf( "Unable to write to '%s' for '%s': %s",
200             $output_file, $file, $! )
201             if $log->is_error();
202             return( FAIL );
203             }
204             $fh->truncate( 0 );
205             $fh->print( $new_perl );
206             $fh->close();
207             return( SUCCESS_CHANGED );
208             }
209              
210             sub get_dist_info
211             {
212             my ( $self, %options ) = @_;
213             my ( $dist_info, $dist_root, $meta_file );
214              
215             $dist_root = $options{ dist_root } || '.';
216              
217             $dist_info = {};
218              
219             if( -r ( $meta_file = File::Spec->catfile( $dist_root, 'META.json' ) ) or
220             -r ( $meta_file = File::Spec->catfile( $dist_root, 'META.yml' ) ) )
221             {
222             $log->debugf( "Reading '%s'", $meta_file )
223             if $log->is_debug();
224             $dist_info->{ meta } = CPAN::Meta->load_file( $meta_file );
225             }
226             else
227             {
228             $log->warningf( "No META.json or META.yml file found, " .
229             "is '%s' a distribution directory?", $dist_root )
230             if $log->is_warning();
231             }
232              
233             if( $dist_info->{ meta } )
234             {
235             $dist_info->{ authors } = [ $dist_info->{ meta }->authors() ];
236              
237             $dist_info->{ authors } =
238             [ map { s/\@/ $options{ antispam } /; $_; }
239             @{$dist_info->{ authors }} ]
240             if $options{ antispam };
241              
242             $log->debug( "Creating license object" )
243             if $log->is_debug();
244             my @licenses = $dist_info->{ meta }->licenses();
245             if( @licenses != 1 )
246             {
247             $log->error( "Pod::Weaver requires one, and only one, " .
248             "license at a time." )
249             if $log->is_error();
250             return;
251             }
252              
253             my $license = $licenses[ 0 ];
254              
255             # Cribbed from Module::Build, really should be in Software::License.
256             my %licenses = (
257             perl => 'Perl_5',
258             perl_5 => 'Perl_5',
259             apache => 'Apache_2_0',
260             apache_1_1 => 'Apache_1_1',
261             artistic => 'Artistic_1_0',
262             artistic_2 => 'Artistic_2_0',
263             lgpl => 'LGPL_2_1',
264             lgpl2 => 'LGPL_2_1',
265             lgpl3 => 'LGPL_3_0',
266             bsd => 'BSD',
267             gpl => 'GPL_1',
268             gpl2 => 'GPL_2',
269             gpl3 => 'GPL_3',
270             mit => 'MIT',
271             mozilla => 'Mozilla_1_1',
272             open_source => undef,
273             unrestricted => undef,
274             restrictive => undef,
275             unknown => undef,
276             );
277              
278             unless( $licenses{ $license } )
279             {
280             $log->errorf( "Unknown license: '%s'", $license )
281             if $log->is_error();
282             return;
283             }
284              
285             $license = $licenses{ $license };
286              
287             my $class = "Software::License::$license";
288             unless( eval "use $class; 1" )
289             {
290             $log->errorf( "Can't load Software::License::$license: %s", $@ )
291             if $log->is_error();
292             return;
293             }
294              
295             $dist_info->{ license } = $class->new( {
296             holder => join( ' & ', @{$dist_info->{ authors }} ),
297             } );
298              
299             $log->debugf( "Using license: '%s'", $dist_info->{ license }->name() )
300             if $log->is_debug();
301              
302             $dist_info->{ dist_version } = $dist_info->{ meta }->version();
303             }
304              
305             return( $dist_info );
306             }
307              
308             sub get_weaver
309             {
310             my ( $self, %options ) = @_;
311             my ( $dist_root, $config_file );
312              
313             $dist_root = $options{ dist_root } || '.';
314             if( -r ( $config_file = File::Spec->catfile( $dist_root, 'weaver.ini' ) ) )
315             {
316             $log->debug( "Initializing weaver from ./weaver.ini" )
317             if $log->is_debug();
318             return( Pod::Weaver->new_from_config( {
319             root => $dist_root,
320             } ) );
321             }
322             $log->warningf( "No '%s' found, using Pod::Weaver defaults, " .
323             "this will most likely insert duplicate sections",
324             $config_file )
325             if $log->is_warning();
326             return( Pod::Weaver->new_with_default_config() );
327             }
328              
329             sub find_files_to_weave
330             {
331             my ( $self, %options ) = @_;
332             my ( $dist_root );
333              
334             $dist_root = $options{ dist_root } || '.';
335              
336             return(
337             File::Find::Rule->ignore_vcs
338             ->not_name( qr/~$/ )
339             ->perl_file
340             ->in(
341             grep { -d $_ }
342             map { File::Spec->catfile( $dist_root, $_ ) }
343             qw/lib bin script/
344             )
345             );
346             }
347              
348             sub weave_distribution
349             {
350             my ( $self, %options ) = @_;
351             my ( $weaver, $dist_info );
352              
353             $dist_info = $self->get_dist_info( %options );
354             $weaver = $self->get_weaver( %options );
355              
356             foreach my $file ( $self->find_files_to_weave() )
357             {
358             $log->noticef( "Weaving file '%s'", $file )
359             if $log->is_notice();
360              
361             $self->weave_file(
362             %options,
363             %{$dist_info},
364             filename => $file,
365             weaver => $weaver,
366             );
367             }
368             }
369              
370             sub _config_dir
371             {
372             my ( $self ) = @_;
373             my ( $leaf_dir, $config_dir );
374              
375             # Following lifted from File::UserDir.
376             # I'd use that directly but it forces creation and population of the dir.
377              
378             # Derive from the caller based on HomeDir naming scheme
379             my $scheme = $File::HomeDir::IMPLEMENTED_BY or
380             die "Failed to find File::HomeDir naming scheme";
381             if( $scheme->isa( 'File::HomeDir::Darwin' ) or
382             $scheme->isa( 'File::HomeDir::Windows' ) )
383             {
384             $leaf_dir = 'App-podweaver';
385             }
386             elsif( $scheme->isa('File::HomeDir::Unix') )
387             {
388             $leaf_dir = '.app-podweaver';
389             }
390             else
391             {
392             die "Unsupported HomeDir naming scheme $scheme";
393             }
394              
395             $config_dir = File::Spec->catdir(
396             File::HomeDir->my_data(),
397             $leaf_dir
398             );
399              
400             return( $config_dir );
401             }
402              
403             sub _config_file
404             {
405             my ( $self ) = @_;
406             my ( $config_dir, $config_file );
407              
408             return( undef ) unless $config_dir = $self->_config_dir();
409              
410             $config_file = File::Spec->catfile( $config_dir, 'podweaver.ini' );
411             return( $config_file );
412             }
413              
414             sub config
415             {
416             my ( $self ) = @_;
417             my ( $config_file, $config );
418              
419             $config_file = $self->_config_file();
420             return( {} ) unless $config_file and -e $config_file;
421             $config = Config::Tiny->read( $config_file ) or
422             die "Error reading '$config_file': " . Config::Tiny->errstr();
423              
424             return( $config );
425             }
426              
427             1;
428              
429             __END__
430              
431             =pod
432              
433             =head1 NAME
434              
435             App::podweaver - Run Pod::Weaver on the files within a distribution.
436              
437             =head1 VERSION
438              
439             version 1.00
440              
441             =head1 SYNOPSIS
442              
443             L<App::podweaver> provides a mechanism to run L<Pod::Weaver> over the files
444             within a distribution, without needing to use L<Dist::Zilla>.
445              
446             Where L<Dist::Zilla> works on a copy of your source code, L<App::podweaver>
447             is intended to modify your source code directly, and as such it is highly
448             recommended that you use the L<Pod::Weaver::PluginBundle::ReplaceBoilerplate>
449             plugin bundle so that you over-write existing POD sections, instead of the
450             default L<Pod::Weaver> behaviour of repeatedly appending.
451              
452             You can configure the L<Pod::Weaver> invocation by providinng a
453             C<weaver.ini> file in the root directory of your distribution.
454              
455             =begin readme
456              
457             =head1 INSTALLATION
458              
459             To install this module, run the following commands:
460              
461             perl Build.PL
462             ./Build
463             ./Build test
464             ./Build install
465              
466             =end readme
467              
468             =head1 BOOTSTRAPPING WITH META.json/META.yml
469              
470             Since the META.json/yml file is often generated with an abstract extracted
471             from the POD, and L<App::podweaver> expects a valid META file for
472             some of the information to insert into the POD, there's a chicken-and-egg
473             situation on the first invocation of either.
474              
475             Running L<App::podweaver> first should produce a POD with an abstract
476             line populated from your C<< # ABSTRACT: >> header, but without additional
477             sections like version and authors.
478             You can then generate your META file as per usual, and then run
479             L<App::podweaver> again to produce the missing sections:
480              
481             $ ./Build distmeta
482             Creating META.yml
483             ERROR: Missing required field 'dist_abstract' for metafile
484             $ podweaver -v
485             No META.json or META.yml file found, are you running in a distribution directory?
486             Processing lib/App/podweaver.pm
487             $ ./Build distmeta
488             Creating META.yml
489             $ podweaver -v
490             Processing lib/App/podweaver.pm
491              
492             This should only be neccessary on newly created distributions as
493             both the META and the neccessary POD abstract should be present
494             subsequently.
495              
496             =for readme stop
497              
498             =head1 METHODS
499              
500             =begin :private
501              
502             =head2 B<FAIL>
503              
504             Indicates the file failed to be woven.
505              
506             =head2 B<SUCCESS_UNCHANGED>
507              
508             Indicates the file was successfully woven but resulted in no changes.
509              
510             =head2 B<SUCCESS_CHANGED>
511              
512             Indicates the file was successfully woven and contained changes.
513              
514             =end :private
515              
516             =head2 I<$success> = B<< App::podweaver->weave_file( >> I<%options> B<)>
517              
518             Runs L<Pod::Weaver> on the given file, merges the generated Pod back
519             into the appropriate place and writes the new file out.
520              
521             C<< App::podweaver->weave_file() >> returns
522             C<< App::podweaver::FAIL >> on failure,
523             and either C<< App::podweaver::SUCCESS_UNCHANGED >> or
524             C<< App::podweaver::SUCCESS_CHANGED >> on success,
525             depending on whether changes needed to be made as a result of
526             the weaving.
527              
528             Currently these constants are not exportable.
529              
530             The following options configure C<< App::podweaver->weave_file() >>:
531              
532             =over
533              
534             =item B<< filename => >> I<$filename> (required)
535              
536             The filename of the file to weave.
537              
538             =item B<< weaver => >> I<$weaver> (required)
539              
540             The L<Pod::Weaver> instance to use for the weaving.
541              
542             =item B<< no_backup => >> I<0> | I<1> (default: 0)
543              
544             If set to a true value, no backup will be made of the original file.
545              
546             =item B<< new => >> I<0> | I<1> (default: 0)
547              
548             If set to a true value, the modified file will be written to the
549             original filename with C<.new> appended, rather than overwriting
550             the original.
551              
552             =item B<< dist_version => >> I<$version>
553              
554             If no C<$VERSION> can be parsed from the file by
555             L<Module::Metadata>, the version supplied in
556             C<dist_version> will be used as a fallback.
557              
558             =back
559              
560             Any additional options are passed untouched to L<Pod::Weaver>.
561              
562             =head2 I<$dist_info> = B<< App::podweaver->get_dist_info( >> I<%options> B<)>
563              
564             Attempts to extract the information needed by L<Pod::Weaver>
565             about the distribution.
566              
567             It does this by examining any C<META.json> or C<META.yml> file
568             it finds, and by expanding various fields found within.
569              
570             Valid options are:
571              
572             =over
573              
574             =item B<< dist_root => >> I<$directory> (default: current working directory)
575              
576             Treats I<$directory> as the root directory of the distribution,
577             where the C<META.json> or C<META.yml> file should be found.
578              
579             If not supplied, this will default to the current working directory.
580              
581             =item B<< antispam => >> I<$string>
582              
583             If set, any @ sign in author emails will be replaced by a space,
584             the given string, and a further space, in an attempt to confuse
585             spammers.
586              
587             For example C<< antispam => 'NOSPAM' >> will transform an email
588             of C<< nobody@127.0.0.1 >> into C<< nobody NOSPAM 127.0.0.1 >>.
589              
590             =back
591              
592             =head2 I<$weaver> = B<< App::podweaver->get_weaver( >> I<%options> B<)>
593              
594             Builds a L<Pod::Weaver> instance, attemping to find a C<weaver.ini>
595             in the distribution root directory.
596              
597             Valid options are:
598              
599             =over
600              
601             =item B<< dist_root => >> I<$directory> (default: current working directory)
602              
603             Treats I<$directory> as the root directory of the distribution,
604             where the C<weaver.ini> file should be found.
605              
606             If not supplied, this will default to the current working directory.
607              
608             =back
609              
610             =head2 I<@files> = B<< App::podweaver->find_files_to_weave( >> I<%options> B<)>
611              
612             Invokes L<File::Find::Rule>, L<File::Find::Rule::VCS> and
613             L<File::Find::Rule::Perl> to return a list of perl files that are
614             candidates to run L<Pod::Weaver> on in the C<lib>, C<bin> and C<script>
615             dirs of the distribution directory.
616              
617             Valid options are:
618              
619             =over
620              
621             =item B<< dist_root => >> I<$directory> (default: current working directory)
622              
623             Treats I<$directory> as the root directory of the distribution.
624              
625             If not supplied, this will default to the current working directory.
626              
627             =back
628              
629             =head2 B<< App::podweaver->weave_distribution( >> I<%options> B<)>
630              
631             Rolls all the other methods together to run L<Pod::Weaver> on the
632             appropriate files within the distribution found in the current
633             working directory.
634              
635             =head2 I<$config> = B<< App::podweaver->config() >>
636              
637             Retrieves the L<Config::Tiny> contents of the user's config file for
638             the application, as found in the C<podweaver.ini> file in the usual
639             place for user configuration files for your OS.
640              
641             (C<~/.app_podweaver/podweaver.ini> for UNIX, C<~/Local Settings/Application
642             Data/App-podweaver/podweaver.ini> under Windows.)
643              
644             =head1 KNOWN ISSUES AND BUGS
645              
646             =over
647              
648             =item META.json/yml bootstrap is a mess
649              
650             The whole bootstrap issue with META.json/yml is ugly.
651              
652             =back
653              
654             =head1 REPORTING BUGS
655              
656             Please report any bugs or feature requests to C<bug-app-podweaver at rt.cpan.org>, or through
657             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=App-podweaver>. I will be notified, and then you'll
658             automatically be notified of progress on your bug as I make changes.
659              
660             =head1 SEE ALSO
661              
662             L<Pod::Weaver>, L<podweaver>.
663              
664             =for readme continue
665              
666             =head1 SUPPORT
667              
668             You can find documentation for this module with the perldoc command.
669              
670             perldoc App::podweaver
671              
672             You can also look for information at:
673              
674             =over 4
675              
676             =item * RT: CPAN's request tracker
677              
678             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=App-podweaver>
679              
680             =item * AnnoCPAN: Annotated CPAN documentation
681              
682             L<http://annocpan.org/dist/App-podweaver>
683              
684             =item * CPAN Ratings
685              
686             L<http://cpanratings.perl.org/d/App-podweaver>
687              
688             =item * Search CPAN
689              
690             L<http://search.cpan.org/dist/App-podweaver/>
691              
692             =back
693              
694             =head1 AUTHOR
695              
696             Sam Graham <libapp-podweaver-perl BLAHBLAH illusori.co.uk>
697              
698             =head1 COPYRIGHT AND LICENSE
699              
700             This software is copyright (c) 2010-2011 by Sam Graham <libapp-podweaver-perl BLAHBLAH illusori.co.uk>.
701              
702             This is free software; you can redistribute it and/or modify it under
703             the same terms as the Perl 5 programming language system itself.
704              
705             =cut