File Coverage

blib/lib/Config/MVP/Slicer.pm
Criterion Covered Total %
statement 65 67 97.0
branch 29 30 96.6
condition 23 26 88.4
subroutine 14 14 100.0
pod 4 4 100.0
total 135 141 95.7


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