File Coverage

blib/lib/Config/MVP/Slicer.pm
Criterion Covered Total %
statement 11 13 84.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 16 18 88.8


line stmt bran cond sub pod time code
1             # vim: set ts=2 sts=2 sw=2 expandtab smarttab:
2             #
3             # This file is part of Config-MVP-Slicer
4             #
5             # This software is copyright (c) 2011 by Randy Stauner.
6             #
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             #
10 6     6   5735 use strict;
  6         13  
  6         241  
11 6     6   35 use warnings;
  6         11  
  6         441  
12              
13             package Config::MVP::Slicer;
14             {
15             $Config::MVP::Slicer::VERSION = '0.302';
16             }
17             BEGIN {
18 6     6   101 $Config::MVP::Slicer::AUTHORITY = 'cpan:RWSTAUNER';
19             }
20             # ABSTRACT: Extract embedded plugin config from parent config
21              
22 6     6   34 use Carp (); # core
  6         15  
  6         163  
23 6     6   3207 use Moose;
  0            
  0            
24              
25              
26             has config => (
27             is => 'ro',
28             isa => 'HashRef',
29             );
30              
31              
32             sub _build_match_name {
33             # "@Bundle/Plugin" =~ "(@Bundle/)*Plugin"
34             return sub { scalar $_[1] =~ m{^(@.+?/)*?\Q$_[0]\E$} };
35             }
36              
37             sub _build_match_package {
38             return sub { $_[0] eq $_[1] };
39             }
40              
41             foreach my $which ( qw( name package ) ) {
42             my $name = "match_$which";
43             has $name => (
44             is => 'bare',
45             isa => 'CodeRef',
46             traits => ['Code'],
47             builder => "_build_$name",
48             handles => {
49             $name => 'execute',
50             },
51             );
52             }
53              
54              
55             has prefix => (
56             is => 'ro',
57             isa => 'RegexpRef | Str',
58             default => '',
59             );
60              
61              
62             has separator => (
63             is => 'ro',
64             isa => 'Str',
65             default => '(.+?)\.(.+?)',
66             );
67              
68              
69             sub separator_regexp {
70             my ($self) = @_;
71             return qr/^${\ $self->prefix }${\ $self->separator }(\[.*?\])?$/;
72             }
73              
74              
75             sub slice {
76             my ($self, $plugin) = @_;
77             # ignore previous config
78             my ($name, $pack) = $self->plugin_info($plugin);
79              
80             # TODO: do we need to do anything to handle mvp_aliases?
81             # TODO: can/should we check $pack->mvp_multivalue_args rather than if ref $value eq 'ARRAY'
82              
83             my $slice = {};
84             my $config = $self->config;
85             my $regexp = $self->separator_regexp;
86              
87             # sort to keep the bracket subscripts in order
88             foreach my $key ( sort keys %$config ){
89             next unless
90             my ($plug, $attr, $array) = ($key =~ $regexp);
91             my $value = $config->{ $key };
92              
93             next unless
94             $self->match_name($plug, $name) ||
95             $self->match_package($plug, $pack);
96              
97             # TODO: should we allow for clearing previous []? $slice->{$attr} = [] if $overwrite;
98              
99             # TODO: $array || ref($prev->{$attr}) eq 'ARRAY'; # or is this handled by merge?
100             $self->_update_hash($slice, $attr, $value, {array => $array});
101             }
102             return $slice;
103             }
104              
105              
106             #* C<join> - A string that will be used to join a new value to any existing value instead of overwriting.
107             # TODO: allow option for reaching into blessed hashref?
108              
109             sub merge {
110             my ($self, $plugin, $opts) = @_;
111             $opts ||= {};
112              
113             my $slice = $opts->{slice} || $self->slice($plugin);
114             my ($name, $class, $conf) = $self->plugin_info($plugin);
115              
116             while( my ($key, $value) = each %$slice ){
117             # merge into hashref
118             if( ref($conf) eq 'HASH' ){
119             $self->_update_hash($conf, $key, $value);
120             }
121             # plugin instance... attempt to update
122             else {
123             # call attribute writer (attribute must be 'rw'!)
124             my $attr = $plugin->meta->find_attribute_by_name($key);
125             if( !$attr ){
126             # TODO: should we be dying here?
127             Carp::croak("Attribute '$key' not found on $name/$class\n");
128             next;
129             }
130             my $type = $attr->type_constraint;
131             my $previous = $plugin->$key;
132             if( $previous ){
133             # FIXME: do we need to check blessed() and/or isa()?
134             if( ref $previous eq 'ARRAY' ){
135             push(@$previous, ref $value eq 'ARRAY' ? @$value : $value);
136             }
137             # if new value was specified as arrayref, attempt to merge
138             elsif( ref $value eq 'ARRAY' ){
139             $plugin->$key( [ $previous, @$value ] );
140             }
141             # is this useful?
142             elsif( $type->name eq 'Str' && $opts->{join} ){
143             $plugin->$key( join($opts->{join}, $previous, $value) );
144             }
145             # TODO: any other types?
146             else {
147             $plugin->$key($value);
148             }
149             }
150             else {
151             $value = [ $value ]
152             if $type->name =~ /^arrayref/i && ref $value ne 'ARRAY';
153              
154             $plugin->$key($value);
155             }
156             }
157             }
158             return $plugin;
159             }
160              
161              
162              
163             sub plugin_info {
164             my ($self, $spec) = @_;
165              
166             # plugin bundles: ['name', 'class', {con => 'fig'}]
167             return @$spec
168             if ref $spec eq 'ARRAY';
169              
170             # plugin instances
171             return ($spec->plugin_name, ref($spec), $spec)
172             if eval { $spec->can('plugin_name') };
173              
174             Carp::croak(qq[Don't know how to handle $spec]);
175             }
176              
177             sub _update_hash {
178             my ($self, $hash, $key, $value, $options) = @_;
179              
180             # concatenate array if
181             if(
182             # we know it should be an array
183             $options->{array} ||
184             # it already is an array
185             (exists($hash->{ $key }) && ref($hash->{ $key }) eq 'ARRAY') ||
186             # the new value is an array
187             ref($value) eq 'ARRAY'
188             ){
189             # if there is an initial value but it's not an array ref, convert it
190             $hash->{ $key } = [ $hash->{ $key } ]
191             if exists $hash->{ $key } && ref $hash->{ $key } ne 'ARRAY';
192              
193             push @{ $hash->{ $key } }, ref($value) eq 'ARRAY' ? @$value : $value;
194             }
195             # else overwrite
196             else {
197             $hash->{ $key } = $value;
198             }
199             }
200              
201             no Moose;
202             __PACKAGE__->meta->make_immutable;
203             1;
204              
205              
206             __END__
207             =pod
208              
209             =encoding utf-8
210              
211             =for :stopwords Randy Stauner ACKNOWLEDGEMENTS cpan testmatrix url annocpan anno bugtracker
212             rt cpants kwalitee diff irc mailto metadata placeholders metacpan
213              
214             =head1 NAME
215              
216             Config::MVP::Slicer - Extract embedded plugin config from parent config
217              
218             =head1 VERSION
219              
220             version 0.302
221              
222             =head1 SYNOPSIS
223              
224             my $slicer = Config::MVP::Slicer->new({
225             config => $parent->config,
226             });
227              
228             # extract a hashref from the parent config without modifying the plugin
229             my $plugin_config = $slicer->slice($plugin);
230              
231             # from plugin bundles:
232             my $plugin_spec = ['Name', 'Package::Name', {default => 'config'}];
233             # update the hashref
234             $slicer->merge($plugin_spec);
235              
236             # with object instances:
237             my $plugger = App::Plugin::Plugger->new({some => 'config'});
238             # update 'rw' attributes
239             $slicer->merge($plugger);
240              
241             =head1 DESCRIPTION
242              
243             This can be used to extract embedded configurations for other plugins
244             out of larger (parent) configurations.
245              
246             A example where this can be useful is plugin bundles
247             (see L<Config::MVP::Assembler::WithBundles>).
248              
249             A bundle loads other plugins with a default configuration
250             that works most of the time, but sometimes you wish you could
251             customize the configuration for one of those plugins
252             without having to remove the plugin from the bundle
253             and re-specify it separately.
254              
255             # mvp config file
256             [@MyBundle]
257             Other::Plugin.setting = new value
258              
259             Now you can accept customizations to plugins into your
260             bundle config and separate them out using this module.
261              
262             =head1 ATTRIBUTES
263              
264             =head2 config
265              
266             This is the main/parent configuration hashref
267             that contains embedded plugin configurations.
268              
269             =head2 match_name
270              
271             This is coderef that determines if a configuration line
272             matches a plugin's name.
273              
274             It can be customized by passing an alternate subroutine reference
275             to the constructor.
276              
277             The sub will receive two arguments:
278              
279             =over 4
280              
281             =item *
282              
283             The plugin name portion of the configuration line
284              
285             =item *
286              
287             The name of the plugin being worked on (provided to L</slice>, for instance).
288              
289             =back
290              
291             The default returns true if the current plugin name matches
292             the name from the config line
293             regardless of any leading "@Bundle/" prefixes in the plugin name
294             (as this is a common convention for bundles).
295              
296             Obviously if the "@Bundle/" prefix is specified in the configuration
297             then it is required to be there for the default sub to match
298             (but multiple other "@Bundle/" prefixes will be allowed before it).
299              
300             # configuration line: "Foo.attr = value"
301              
302             $slicer->match_name("Foo", "Foo"); # true
303             $slicer->match_name("Foo", "@Bar/Foo"); # true
304             $slicer->match_name("Foo", "Bar"); # false
305              
306             # configuration line: "@Bar/Foo.attr = value"
307              
308             $slicer->match_name("@Bar/Foo", "Foo"); # false
309             $slicer->match_name("@Bar/Foo", "@Bar/Foo"); # true
310             $slicer->match_name("@Bar/Foo", "@Baz/@Bar/Foo"); # true
311             $slicer->match_name("@Bar/Foo", "@Baz/Foo"); # false
312              
313             Subclasses can define C<_build_match_name>
314             (which should return a C<sub>) to overwrite the default.
315              
316             =head2 match_package
317              
318             This works like L</match_name>
319             except that the configuration line is compared
320             to the plugin's package (class).
321              
322             The default returns true if the two values are equal and false otherwise.
323              
324             If you want to match by package rather than name
325             and you expand packages with (for example) a string prefix
326             you may need to set this to something like:
327              
328             match_package => sub { rewrite_prefix($_[0]) eq $_[1] }
329              
330             Subclasses can define C<_build_match_package>
331             (which should return a C<sub>) to overwrite the default.
332              
333             =head2 prefix
334              
335             Regular expression that should match at the beginning of a key
336             before the module name and attribute:
337              
338             # prefix => 'dynamic\.'
339             # { 'dynamic.Module::Name.attr' => 'value' }
340              
341             This can be a string or a compiled regular expression (C<qr//>).
342              
343             The default is no prefix (empty string C<''>).
344              
345             =head2 separator
346              
347             A regular expression that will capture
348             the package name in C<$1> and
349             the attribute name in C<$2>.
350              
351             The default (C<(.+?)\.(.+?)>)
352             separates plugin name from attribute name with a dot:
353              
354             'Module::Name.attribute'
355             '-Plugin.attr'
356              
357             B<NOTE>: The regexp should B<not> be anchored since L</separator_regexp>
358             uses it as the middle piece of a larger regexp
359             (to add L</prefix> and the possible array bracket suffix).
360             Also beware of using a regexp that greedily matches the array bracket suffix
361             as that can confuse things as well.
362              
363             =head1 METHODS
364              
365             =head2 separator_regexp
366              
367             Returns a compiled regular expression (C<qr//>)
368             combining L</prefix>, L</separator>,
369             and the possible trailing array specification (C<\[.*?\]>).
370              
371             =head2 slice
372              
373             $slicer->slice($plugin);
374              
375             Return a hashref of the config arguments for the plugin
376             determined by C<$plugin>.
377              
378             This is a slice of the L</config> attribute
379             appropriate for the plugin passed to the method.
380              
381             Starting with a config hashref of:
382              
383             {
384             'APlug:attr1' => 'value1',
385             'APlug:second' => '2nd',
386             'OtherPlug:attr => '0'
387             }
388              
389             Passing a plugin instance of C<'APlug'>
390             (or an arrayref of C<< ['APlug', 'Full::Package::APlug', {}] >>)
391             would return:
392              
393             {
394             'attr1' => 'value1',
395             'second' => '2nd'
396             }
397              
398             =head2 merge
399              
400             $slicer->merge($plugin, \%opts);
401              
402             Get the config slice (see L</slice>),
403             then attempt to merge it into the plugin.
404              
405             If C<$plugin> is an arrayref the hashref will be modified.
406             If it is an object it's attributes should be writable (C<'rw'>).
407              
408             This will append to array references
409             if it was specified as an array
410             or if a preexisting value is an arrayref.
411              
412             Returns the modified C<$plugin> for convenience.
413              
414             Possible options:
415              
416             =over 4
417              
418             =item *
419              
420             C<slice> - A hashref like that returned from L</slice>. If not present, L</slice> will be called.
421              
422             =back
423              
424             =head2 plugin_info
425              
426             $slicer->plugin_info($plugin);
427              
428             Used by other methods to normalize the information about a plugin.
429             Returns a list of C<< ($name, $package, \%config) >>.
430              
431             If C<$plugin> is an arrayref it will simply dereference it.
432             This can be useful for processing the results of plugin bundles.
433              
434             If C<$plugin> is an instance of a plugin that has a C<plugin_name>
435             method it will construct the list from that method, C<ref>,
436             and the instance itself.
437              
438             =for test_synopsis my ($parent, $plugin);
439              
440             =head1 CONFIGURATION SYNTAX
441              
442             Often configurations come from an C<ini> file and look like this:
443              
444             [PluginName]
445             option = value
446              
447             This gets converted to a hashref:
448              
449             PluginName->new({ option => 'value' });
450              
451             To embed configuration for other plugins:
452              
453             [@BigBundle]
454             bundle_option = value
455             Bundled::Plugin.option = other value
456              
457             The simple 'bundle_option' attribute is for C<@BigBundle>,
458             and the bundle can slice out the C<Bundled::Plugin> configuration
459             and merge it in to that plugin's configuration.
460              
461             Prefixes can be used (see L</prefix>).
462             In this example the prefix is set as C<"plug.">.
463              
464             [@Foo]
465             plug.Bundled::Plugin.attr = value
466              
467             Due to limitations of this dynamic passing of unknown options
468             (otherwise known as a I<hack>)
469             values that are arrays cannot be declared ahead of time by the bundle.
470             You can help out by specifying that an attribute should be an array:
471              
472             [@Bar]
473             Baz.quux[0] = part 1
474             Baz.quux[1] = part 2
475              
476             This is required because each line will end up in a hashref:
477              
478             { "quux[0]" => "part 1", "quxx[1]" => "part 2" }
479              
480             The subscripts inside the brackets are used for sorting but otherwise ignored.
481             The L</slice> method will sort the keys (B<alphabetically>) to produce:
482              
483             { quux => ["part 1", "part 2"] }
484              
485             For simplicity the keys are sorted B<alphabetically>
486             because C<quux[1.9]> and C<quux[1.10]>
487             probably won't sort the way you intended anyway,
488             so just keep things simple:
489              
490             [@Bundle]
491             Plug.attr[0] = part 1
492             Plug.attr[1] = part 2
493             Plug.other[09] = part 1
494             Plug.other[10] = part 2
495             Plug.alpha[a] = part 1
496             Plug.alpha[b] = part 2
497             Plug.alpha[bc] = part 3
498             Plug.single[] = subscript not required; only used for sorting
499              
500             =head1 SUPPORT
501              
502             =head2 Perldoc
503              
504             You can find documentation for this module with the perldoc command.
505              
506             perldoc Config::MVP::Slicer
507              
508             =head2 Websites
509              
510             The following websites have more information about this module, and may be of help to you. As always,
511             in addition to those websites please use your favorite search engine to discover more resources.
512              
513             =over 4
514              
515             =item *
516              
517             Search CPAN
518              
519             The default CPAN search engine, useful to view POD in HTML format.
520              
521             L<http://search.cpan.org/dist/Config-MVP-Slicer>
522              
523             =item *
524              
525             RT: CPAN's Bug Tracker
526              
527             The RT ( Request Tracker ) website is the default bug/issue tracking system for CPAN.
528              
529             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Config-MVP-Slicer>
530              
531             =item *
532              
533             CPAN Ratings
534              
535             The CPAN Ratings is a website that allows community ratings and reviews of Perl modules.
536              
537             L<http://cpanratings.perl.org/d/Config-MVP-Slicer>
538              
539             =item *
540              
541             CPAN Testers
542              
543             The CPAN Testers is a network of smokers who run automated tests on uploaded CPAN distributions.
544              
545             L<http://www.cpantesters.org/distro/C/Config-MVP-Slicer>
546              
547             =item *
548              
549             CPAN Testers Matrix
550              
551             The CPAN Testers Matrix is a website that provides a visual overview of the test results for a distribution on various Perls/platforms.
552              
553             L<http://matrix.cpantesters.org/?dist=Config-MVP-Slicer>
554              
555             =item *
556              
557             CPAN Testers Dependencies
558              
559             The CPAN Testers Dependencies is a website that shows a chart of the test results of all dependencies for a distribution.
560              
561             L<http://deps.cpantesters.org/?module=Config::MVP::Slicer>
562              
563             =back
564              
565             =head2 Bugs / Feature Requests
566              
567             Please report any bugs or feature requests by email to C<bug-config-mvp-slicer at rt.cpan.org>, or through
568             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Config-MVP-Slicer>. You will be automatically notified of any
569             progress on the request by the system.
570              
571             =head2 Source Code
572              
573              
574             L<https://github.com/rwstauner/Config-MVP-Slicer>
575              
576             git clone https://github.com/rwstauner/Config-MVP-Slicer.git
577              
578             =head1 AUTHOR
579              
580             Randy Stauner <rwstauner@cpan.org>
581              
582             =head1 COPYRIGHT AND LICENSE
583              
584             This software is copyright (c) 2011 by Randy Stauner.
585              
586             This is free software; you can redistribute it and/or modify it under
587             the same terms as the Perl 5 programming language system itself.
588              
589             =cut
590