File Coverage

lib/Dist/Zilla/Util/ExpandINI.pm
Criterion Covered Total %
statement 79 115 68.7
branch 19 28 67.8
condition 4 6 66.6
subroutine 15 21 71.4
pod 3 3 100.0
total 120 173 69.3


line stmt bran cond sub pod time code
1 10     10   2964882 use 5.006;
  10         24  
  10         277  
2 10     10   34 use strict;
  10         11  
  10         248  
3 10     10   30 use warnings;
  10         17  
  10         542  
4              
5             package Dist::Zilla::Util::ExpandINI;
6              
7             our $VERSION = '0.003001';
8              
9             # ABSTRACT: Read an INI file and expand bundles as you go.
10              
11             our $AUTHORITY = 'cpan:KENTNL'; # AUTHORITY
12              
13 10     10   36 use Carp qw( croak );
  10         11  
  10         556  
14 10     10   4658 use Moo 1.000008 qw( has );
  10         99373  
  10         45  
15 10     10   31654 use Scalar::Util qw( blessed );
  10         14  
  10         550  
16 10     10   4180 use Dist::Zilla::Util::BundleInfo 1.001000;
  10         147938  
  10         8488  
17              
18             has '_data' => (
19             is => 'rw',
20             lazy => 1,
21             default => sub { [] },
22             );
23              
24             has '_reader_class' => (
25             is => 'ro',
26             lazy => 1,
27             default => sub {
28             require Dist::Zilla::Util::ExpandINI::Reader;
29             return 'Dist::Zilla::Util::ExpandINI::Reader';
30             },
31             handles => {
32             _read_file => read_file =>,
33             _read_string => read_string =>,
34             _read_handle => read_handle =>,
35             },
36             );
37             has '_writer_class' => (
38             is => 'ro',
39             lazy => 1,
40             default => sub {
41             require Dist::Zilla::Util::ExpandINI::Writer;
42             return 'Dist::Zilla::Util::ExpandINI::Writer';
43             },
44             handles => {
45             _write_file => write_file =>,
46             _write_string => write_string =>,
47             _write_handle => write_handle =>,
48             },
49             );
50              
51             has 'include_does' => (
52             is => 'ro',
53             default => sub { [] },
54             );
55              
56             has 'exclude_does' => (
57             is => 'ro',
58             default => sub { [] },
59             );
60              
61             my $valid_comment_types = {
62             'all' => 1,
63             'none' => 1,
64             'authordeps' => 1,
65             };
66              
67             has 'comments' => (
68             is => 'rw',
69             isa => sub { croak 'comments accepts all, none or authordeps' unless exists $valid_comment_types->{ $_[0] } },
70             default => sub { return 'all' },
71             );
72              
73             sub _load_file {
74 0     0   0 my ( $self, $name ) = @_;
75 0         0 $self->_data( $self->_read_file($name) );
76 0         0 return;
77             }
78              
79             sub _load_string {
80 9     9   52 my ( $self, $content ) = @_;
81 9         29 $self->_data( $self->_read_string($content) );
82 9         3394 return;
83             }
84              
85             sub _load_handle {
86 0     0   0 my ( $self, $handle ) = @_;
87 0         0 $self->_data( $self->_read_handle($handle) );
88 0         0 return;
89             }
90              
91             sub _store_file {
92 0     0   0 my ( $self, $name ) = @_;
93 0         0 $self->_write_file( $self->_data, $name );
94 0         0 return;
95             }
96              
97             sub _store_string {
98 9     9   22906 my ($self) = @_;
99 9         204 return $self->_write_string( $self->_data );
100             }
101              
102             sub _store_handle {
103 0     0   0 my ( $self, $handle ) = @_;
104 0         0 $self->_write_handle( $self->_data, $handle );
105 0         0 return;
106             }
107              
108              
109              
110              
111              
112              
113              
114              
115              
116              
117             sub filter_file {
118 0     0 1 0 my ( $class, $input_fn, $output_fn ) = @_;
119 0         0 my $self = $class;
120 0 0       0 if ( not blessed $class ) {
121 0         0 $self = $class->new;
122             }
123 0         0 local $self->{_data} = {}; # contamination avoidance.
124 0         0 $self->_load_file($input_fn);
125 0         0 $self->_expand();
126 0         0 $self->_store_file($output_fn);
127 0         0 return;
128             }
129              
130              
131              
132              
133              
134              
135              
136              
137              
138             sub filter_handle {
139 0     0 1 0 my ( $class, $input_fh, $output_fh ) = @_;
140 0         0 my $self = $class;
141 0 0       0 if ( not blessed $class ) {
142 0         0 $self = $class->new;
143             }
144 0         0 local $self->{_data} = {}; # contamination avoidance.
145 0         0 $self->_load_handle($input_fh);
146 0         0 $self->_expand();
147 0         0 $self->_store_handle($output_fh);
148 0         0 return;
149             }
150              
151              
152              
153              
154              
155              
156              
157              
158              
159             sub filter_string {
160 1     1 1 11 my ( $class, $input_string ) = @_;
161 1         1 my $self = $class;
162 1 50       5 if ( not blessed $class ) {
163 1         5 $self = $class->new;
164             }
165 1         4 local $self->{_data} = {}; # contamination avoidance.
166 1         3 $self->_load_string($input_string);
167 1         3 $self->_expand();
168 1         3 return $self->_store_string;
169             }
170              
171             sub _includes_module {
172 119     119   90 my ( $self, $module ) = @_;
173 119 100       80 return 1 unless @{ $self->include_does };
  119         313  
174 17         99 require Module::Runtime;
175 17         43 Module::Runtime::require_module($module);
176 17         1061331 for my $include ( @{ $self->include_does } ) {
  17         105  
177 104 100       8766 return 1 if $module->does($include);
178             }
179 2         49 return;
180             }
181              
182             sub _excludes_module {
183 117     117   108 my ( $self, $module ) = @_;
184 117 50       59 return unless @{ $self->exclude_does };
  117         310  
185 0         0 require Module::Runtime;
186 0         0 Module::Runtime::require_module($module);
187 0         0 for my $exclude ( @{ $self->exclude_does } ) {
  0         0  
188 0 0       0 return 1 if $module->does($exclude);
189             }
190 0         0 return;
191             }
192              
193             sub _include_module {
194 119     119   113 my ( $self, $module ) = @_;
195 119 100       134 return unless $self->_includes_module($module);
196 117 50       442 return if $self->_excludes_module($module);
197 117         174 return 1;
198             }
199              
200             sub _expand {
201 7     7   29 my ($self) = @_;
202 7         9 my @out;
203 7         10 my @in = @{ $self->_data };
  7         107  
204 7         54 while (@in) {
205 28         39 my $tip = shift @in;
206              
207 28         64 $tip->{comment_lines} = $self->_clean_comment_lines( $tip->{comment_lines} );
208              
209 28 100 66     1900 if ( $tip->{name} and '_' eq $tip->{name} ) {
210 7         11 push @out, $tip;
211 7         16 next;
212             }
213 21 100 66     112 if ( $tip->{package} and $tip->{package} !~ /\A\@/msx ) {
214 14         18 push @out, $tip;
215 14         27 next;
216             }
217              
218             # Handle bundle
219 7         53 my $bundle = Dist::Zilla::Util::BundleInfo->new(
220             bundle_name => $tip->{package},
221             bundle_payload => $tip->{lines},
222             );
223 7         63585 for my $plugin ( $bundle->plugins ) {
224 119 100       51655 next unless $self->_include_module( $plugin->module );
225 117         238 my $rec = { package => $plugin->short_module };
226 117         817 $rec->{name} = $plugin->name;
227 117         184 $rec->{lines} = [ $plugin->payload_list ];
228 117         764 push @out, $rec;
229             }
230              
231             # Inject any comments from under a bundle
232 7         168 $out[-1]->{comment_lines} = $tip->{comment_lines};
233              
234             }
235 7         121 $self->_data( \@out );
236 7         53 return;
237             }
238              
239             sub _clean_comment_lines {
240 28     28   32 my ( $self, $lines ) = @_;
241 28 100       342 return $lines if q[all] eq $self->comments;
242 8 100       797 return [ grep { /\A\s*authordep\s+/msx } @{$lines} ] if q[authordeps] eq $self->comments;
  4         14  
  4         26  
243 4         19 return [];
244             }
245             1;
246              
247             __END__
248              
249             =pod
250              
251             =encoding UTF-8
252              
253             =head1 NAME
254              
255             Dist::Zilla::Util::ExpandINI - Read an INI file and expand bundles as you go.
256              
257             =head1 VERSION
258              
259             version 0.003001
260              
261             =head1 SYNOPSIS
262              
263             # Write a dist.ini with a bundle anywhere you like
264             my $string = <<"EOF";
265             name = Foo
266             version = 1.000
267              
268             [@Some::Author]
269             EOF
270              
271             path('dist.ini.meta')->spew($string);
272              
273             # Generate a copy with bundles inlined.
274             use Dist::Zilla::Util::ExpandINI;
275             Dist::Zilla::Util::ExpandINI->filter_file( 'dist.ini.meta' => 'dist.ini' );
276              
277             # Hurrah, dist.ini has all the things!
278              
279             # Advanced Usage:
280             my $filter = Dist::Zilla::Util::ExpandINI->new(
281             include_does => [ 'Dist::Zilla::Role::FileGatherer', ],
282             exclude_does => [ 'Dist::Zilla::Role::Releaser', ],
283             );
284             $filter->filter_file( 'dist.ini.meta' => 'dist.ini' );
285              
286             =head1 DESCRIPTION
287              
288             This module builds upon the previous work L<< C<:Util::BundleInfo>|Dist::Zilla::Util::BundleInfo >> ( Which can extract
289             configuration from a bundle in a manner similar to how C<dzil> does it ) and integrates it with some I<very> minimal C<INI>
290             handling to provide a tool capable of generating bundle-free C<dist.ini> files from bundle-using C<dist.ini> files!
291              
292             At present its very naïve and only keeps semantic ordering, and I've probably gotten something wrong due to cutting the
293             complexity of Config::MVP out of the loop.
294              
295             But at this stage, bundles are the I<only> thing modified in transit.
296              
297             Every thing else is practically a token-level copy-paste.
298              
299             =head1 METHODS
300              
301             =head2 C<filter_file>
302              
303             # $source , $dest
304             Dist::Zilla::Util::ExpandINI->filter_file('source.ini','target.ini');
305              
306             Reads C<$source>, performs expansions, and emits C<$dest>
307              
308             =head2 C<filter_handle>
309              
310             Dist::Zilla::Util::ExpandINI->filter_handle($reader,$writer);
311              
312             Reads C<$reader>, performs expansions, and emits to C<$writer>
313              
314             =head2 C<filter_string>
315              
316             my $return = Dist::Zilla::Util::ExpandINI->filter_string($source);
317              
318             Decodes C<$source>, performs expansions, and returns expanded source.
319              
320             =head1 ATTRIBUTES
321              
322             =head2 C<include_does>
323              
324             An C<ArrayRef> of C<Role>s to include in the emitted C<INI> from the source C<INI>.
325              
326             If this C<ArrayRef> is empty, all C<Plugin>s will be included.
327              
328             This is the default behavior.
329              
330             ->new( include_does => [ 'Dist::Zilla::Role::VersionProvider', ] );
331              
332             ( C<API> Since C<0.002000> )
333              
334             =head2 C<exclude_does>
335              
336             An C<ArrayRef> of C<Role>s to I<exclude> from the emitted C<INI>.
337              
338             If this C<ArrayRef> is empty, I<no> C<Plugin>s will be I<excluded>
339              
340             This is the default behavior.
341              
342             ->new( exclude_does => [ 'Dist::Zilla::Role::Releaser', ] );
343              
344             ( C<API> Since C<0.002000> )
345              
346             =head2 C<comments>
347              
348             This attribute controls how comments are handled.
349              
350             =over 4
351              
352             =item *
353              
354             C<all> - All comments are copied ( B<Default> )
355              
356             =item *
357              
358             C<authordeps> - Only comments that look like C<Dist::Zilla> C<AuthorDeps> are copied.
359              
360             =item *
361              
362             C<none> - No comments are copied.
363              
364             =back
365              
366             ( C<API> Since C<0.003000> )
367              
368             =head1 COMMENT PRESERVATION
369              
370             Comments are ( since C<v0.002000> ) arbitrarily supported in a very basic way.
371             But the behavior may be surprising.
372              
373             [SectionHeader]
374             BODY
375             [SectionHeader]
376             BODY
377              
378             Is how C<Config::INI> understands its content. So comment parsing is implemented as
379              
380             BODY:
381             comments: [ "A", "B", "C" ],
382             params: [ "x=y","foo=bar" ]
383              
384             So:
385              
386             [Header]
387             ;A
388             x = y ; Trailing Note
389             ;B
390             foo = bar ; Trailing Note
391              
392             ;Remark About Header2
393             [Header2]
394              
395             Is re-serialized as:
396              
397             [Header]
398             ;A
399             ;B
400             ;Remark About Header2
401             x = y
402             foo = bar
403              
404             [Header2]
405              
406             This behavior may seem surprising, but its surprising only if you
407             have assumptions about how C<INI> parsing works.
408              
409             This also applies and has strange effects with bundles:
410              
411             [Header]
412             x = y
413              
414             ; CommentAboutBundle
415             [@Bundle]
416             ; More Comments About Bundle
417              
418             [Header2]
419              
420             This expands as:
421              
422             [Header]
423             ; CommentAboutBundle
424             x = y
425              
426             [BundleHeader1]
427             arg = value
428              
429             [BundleHeader2]
430             arg = value
431              
432             [BundleHeader3]
433             ; More Comments About Bundle
434             arg = value
435              
436             [Header2]
437              
438             And also note, at this time, only whole-line comments are preserved. Suffix comments are stripped.
439              
440             =head1 AUTHOR
441              
442             Kent Fredric <kentnl@cpan.org>
443              
444             =head1 COPYRIGHT AND LICENSE
445              
446             This software is copyright (c) 2015 by Kent Fredric <kentfredric@gmail.com>.
447              
448             This is free software; you can redistribute it and/or modify it under
449             the same terms as the Perl 5 programming language system itself.
450              
451             =cut