File Coverage

blib/lib/Brickyard.pm
Criterion Covered Total %
statement 125 132 94.7
branch 46 58 79.3
condition 4 4 100.0
subroutine 16 17 94.1
pod 8 8 100.0
total 199 219 90.8


line stmt bran cond sub pod time code
1              
2             package Brickyard;
3              
4 4     4   155902 use 5.010;
  4         18  
  4         192  
5 4     4   24 use warnings;
  4         10  
  4         133  
6 4     4   24 use strict;
  4         12  
  4         604  
7              
8             our $VERSION = '2.0.0';
9              
10 4         60 use Brickyard::Accessor rw =>
11 4     4   2849 [qw(base_package expand plugins plugins_role_cache)];
  4         13  
12 4     4   26 use Carp qw(croak);
  4         8  
  4         8981  
13              
14             sub new {
15 15     15 1 24857 my $class = shift;
16 15         147 bless {
17             base_package => 'MyApp',
18             expand => [],
19             plugins => [],
20             plugins_role_cache => {},
21             @_
22             }, $class;
23             }
24              
25             sub plugins_with {
26 4     4 1 18 my ($self, $role) = @_;
27 4         8 $role = $self->expand_package($role);
28 6         33 $self->plugins_role_cache->{$role} ||=
29 4   100     13 [ grep { $_->DOES($role) } @{ $self->plugins } ];
  2         7  
30 4         20 @{ $self->plugins_role_cache->{$role} };
  4         10  
31             }
32              
33             sub plugins_agree {
34 0     0 1 0 my ($self, $role, $code) = @_;
35 0         0 my @plugins = $self->plugins_with($role);
36 0 0       0 return unless @plugins;
37 0         0 for (@plugins) {
38              
39             # $code can use $_->foo($bar)
40 0 0       0 return unless $code->();
41             }
42 0         0 1;
43             }
44              
45             sub reset_plugins {
46 1     1 1 1019 my $self = shift;
47 1         6 $self->plugins([]);
48 1         5 $self->plugins_role_cache({});
49             }
50              
51             sub parse_ini {
52 7     7 1 17 my ($self, $ini, $callback) = @_;
53 7   100 20   63 $callback //= sub { $_[0] }; # default: identity function
  20         48  
54 7         27 my @result = ([ '_', '_', {} ]);
55 7         13 my $counter = 0;
56 7         204 foreach (split /(?:\015{1,2}\012|\015|\012)/, $ini) {
57 47         53 $counter++;
58 47 100       155 next if /^\s*(?:\#|\;|$)/; # Skip comments and empty lines
59 34         50 s/\s\;\s.+$//g; # Remove inline comments
60              
61             # Handle section headers
62 34 100       109 if (/^\s*\[\s*(.+?)\s*\]\s*$/) {
63 10         40 push @result, [ $1, $1, {} ];
64 10         19 next;
65             }
66              
67             # Handle properties
68 24 50       162 if (/^\s*([^=]+?)\s*=\s*(.*?)\s*$/) {
69 24         90 my ($key, $value) = ($1, $2);
70 24         48 $value = $callback->($value);
71 24         60 my $section = $result[-1][2];
72              
73             # if a property is seen multiple times, it becomes an array
74 24 100       60 if (exists $section->{$key}) {
75 6 100       33 $section->{$key} = [ $section->{$key} ]
76             unless ref $section->{$key} eq 'ARRAY';
77 6         9 push @{ $section->{$key} } => $value;
  6         17  
78             } else {
79 18         55 $section->{$key} = $value;
80             }
81 24         50 next;
82             }
83 0         0 die "Syntax error in INI file at line $counter: '$_'";
84             }
85 7         37 \@result;
86             }
87              
88             # appropriated from CGI::Expand
89             sub _expand_hash {
90 17     17   28 my $flat = $_[1];
91 17         26 my $deep = {};
92 17         60 for my $name (keys %$flat) {
93 32         94 my ($first, @segments) = split /\./, $name;
94 32         85 my $box_ref = \$deep->{$first};
95 32         54 for (@segments) {
96 28 100       94 if (/^(0|[1-9]\d*)$/) {
97 11 100       24 $$box_ref = [] unless defined $$box_ref;
98 11 100       43 croak "param clash for $name($_)"
99             unless ref $$box_ref eq 'ARRAY';
100 10         25 $box_ref = \($$box_ref->[$1]);
101             } else {
102 17 100       51 $$box_ref = {} unless defined $$box_ref;
103 17 50       43 croak "param clash for $name($_)"
104             unless ref $$box_ref eq 'HASH';
105 17         55 $box_ref = \($$box_ref->{$_});
106             }
107             }
108 31 50       107 croak "param clash for $name value $flat->{$name}"
109             if defined $$box_ref;
110 31         81 $$box_ref = $flat->{$name};
111             }
112 16         65 $deep;
113             }
114              
115             sub expand_package {
116 31     31 1 59 my $self = shift;
117 31         49 local $_ = shift;
118 31 100       152 my $base = s/^\*// ? 'Brickyard' : $self->base_package;
119 31 100       158 return $_ if s/^@(?=\w)/$base\::PluginBundle::/;
120 24 100       101 return $_ if s/^-(?=\w)/$base\::Role::/;
121 19 100       60 return $_ if s/^=(?=\w)//;
122 18         30 for my $expand (@{ $self->expand }) {
  18         57  
123 11         20 my $before = $_;
124 11         694 eval $expand;
125 11 100       50 return $_ if $_ ne $before;
126 8 50       31 die $@ if $@;
127             }
128 15         65 "$base\::Plugin::$_";
129             }
130              
131             sub _read_config_file {
132 4     4   30 my ($self, $file) = @_;
133 4 50       186 open my $fh, '<', $file or die "can't open $file for reading: $!\n";
134 4         7 my $config = do { local $/; <$fh> };
  4         14  
  4         133  
135 4 50       71 close $fh or die "can't close $file: $!\n";
136 4         27 $config;
137             }
138              
139             sub _merge_configs {
140 4     4   7 my ($self, $merged_config, $new_config) = @_;
141 4 100       19 return $new_config unless ref $merged_config eq 'ARRAY';
142 1         3 for my $new_section (@$new_config) {
143 2         3 my ($local_name, $plugin_config) = @{$new_section}[0,2];
  2         5  
144 2 100       7 if ($local_name eq '_') {
145             # assume the merged config's root section is at the start
146 1         9 $merged_config->[0][2] = {
147 1         3 %{ $merged_config->[0][2] },
148             %$plugin_config
149             };
150             } else {
151 1         4 push @$merged_config => $new_section;
152             }
153             }
154 1         5 $merged_config;
155             }
156              
157             sub init_from_config {
158 4     4 1 33 my ($self, $config, $root, $callback) = @_;
159 4 100       42 if (ref $config eq 'SCALAR') {
160             # $config is a reference to the INI string
161 1         6 my $this_config = $self->parse_ini($$config, $callback);
162 1         6 $_->[2] = $self->_expand_hash($_->[2]) for @$this_config;
163 1         6 $self->init_from_config_structure($this_config, $root, $callback);
164             } else {
165             # $config is a filename
166 3         6 my $merged_config;
167 3         16 my @files = split /:/ => $config;
168 3         10 for my $file (@files) {
169 4         24 my $this_config = $self->parse_ini($self->_read_config_file($file), $callback);
170 4         23 $_->[2] = $self->_expand_hash($_->[2]) for @$this_config;
171 4         14 $merged_config = $self->_merge_configs($merged_config, $this_config);
172             }
173 3         13 $self->init_from_config_structure($merged_config, $root, $callback);
174             }
175             }
176              
177             sub init_from_config_structure {
178 8     8 1 65 my ($self, $config, $root, $callback) = @_;
179 8         17 for my $section (@$config) {
180 21         47 my ($local_name, $name, $plugin_config) = @$section;
181 21 100       55 if ($local_name eq '_') {
182              
183             # Global container configuration
184 4         23 while (my ($key, $value) = each %$plugin_config) {
185 6 100       17 if ($key eq 'expand') {
186 2 50       4 push @{ $self->expand },
  2         11  
187             ref $value eq 'ARRAY' ? @$value : $value;
188             } else {
189 4         21 $root->$key($value);
190             }
191             }
192             } else {
193 17         51 my $package = $section->[1] = $self->expand_package($name);
194 17         1060 eval "require $package";
195 17 50       32611 die "Cannot require $package: $@" if $@;
196 17 100       73 if ($package->DOES('Brickyard::Role::PluginBundle')) {
197 4         58 my $bundle = $package->new(brickyard => $self, %$plugin_config);
198 4         17 $self->init_from_config_structure($bundle->bundle_config, $root);
199             } else {
200 13         109 push @{ $self->plugins } => $package->new(
  13         115  
201             name => $local_name,
202             brickyard => $self,
203             %$plugin_config
204             );
205             }
206             }
207             }
208             }
209             1;
210              
211             =head1 NAME
212              
213             Brickyard - Plugin system based on roles
214              
215             =head1 SYNOPSIS
216              
217             use Brickyard;
218             my $brickyard = Brickyard->new(base_package => 'My::App');
219             my $root_config = MyApp::RootConfig->new;
220             $brickyard->init_from_config('myapp.ini', $root_config);
221             $_->some_method for $brickyard->plugins_with(-SomeRole);
222              
223             =head1 DESCRIPTION
224              
225             This is a lightweight plugin system based on roles. It does not use Moose but
226             relies on C instead, and very few other modules.
227              
228             It takes its inspiration from L, but has much less flexibility
229             and therefore is also much less complex.
230              
231             =head1 METHODS
232              
233             =head2 new
234              
235             Constructs a new object. Takes an optional hash of arguments to initialize the
236             object.
237              
238             =head2 base_package
239              
240             Read-write accessor for the base package name that is used in
241             C. Defaults to C.
242              
243             =head2 parse_ini
244              
245             Takes a string that contains configuration in C format and parses it into
246             an array of configuration sections. It returns a reference to that array.
247              
248             Using an array, as opposed to a hash, ensures that the section order is
249             preserved, so we know in which order to process plugins in L's
250             C method.
251              
252             Each array element corresponds to an C section. Each section is itself a
253             reference to an array with three elements:
254              
255             The first element is the section name. The second element is the package name
256             of the plugin; it is obtained by expanding the section name using
257             C. The third element is a reference to a plugin
258             configuration hash; it is the section's payload. If a section payload key
259             occurs several times, it is turned into an array reference in the plugin
260             configuration hash.
261              
262             The first section is the global section, denoted by the name C<_>. Any payload
263             in the C configuration that occurs before the first section ends up in
264             this section.
265              
266             For example:
267              
268             ; A comment
269             name = Foobar
270              
271             [@Default]
272              
273             [Some::Thing]
274             foo = bar
275             baz = 43
276             baz = blah
277              
278             is parsed into this structure:
279              
280             [ '_', 'MyApp::Plugin::_', { name => 'Foobar' } ],
281             [ '@Default', 'MyApp::PluginBundle::Default', {} ],
282             [ 'Some::Thing',
283             'MyApp::Plugin::Some::Thing',
284             { 'baz' => [ '43', 'blah' ],
285             'foo' => 'bar'
286             }
287             ]
288              
289             What if you want to pass more complex configuration like a hash of
290             arrays? An C file is basically just a key-value mapping. In that
291             case you can use a special notation for the key where you use dots to
292             separate the individual elements - array indices and hash keys. For
293             example:
294              
295             foo.0.web.1 = bar
296             foo.0.web.2 = baz
297             foo.0.mailto = the-mailto
298             foo.1.url = the-url
299              
300             And this would be parsed into this structure:
301              
302             foo => [
303             { web => [ undef, 'bar', 'baz' ],
304             mailto => 'the-mailto',
305             },
306             { url => 'the-url' }
307             ]
308              
309             =head2 expand_package
310              
311             Takes an abbreviated package name and expands it into the real package
312             name. C section names are processed this way so you don't have to
313             repeat common prefixes all the time.
314              
315             If C<@> occurs at the start of the string, it is replaced by the base
316             name plus <::PluginBundle::>.
317              
318             A C<-> is replaced by the base name plus C<::Role::>.
319              
320             A C<=> is replaced by the empty string, so the remainder is returned
321             unaltered.
322              
323             If the package name still hasn't been altered by the expansions
324             mentioned above, custom expansions are applied; see below.
325              
326             As a fallback, the base name plus C<::Plugin::> is prepended.
327              
328             The base name is normally whatever C returns, but if
329             the string starts with C<*>, the asterisk is deleted and C
330             is used for the base name.
331              
332             A combination of the default prefixes is not expanded, so C<@=>, for
333             example, is treated as the fallback case, which is probably not what
334             you intended.
335              
336             Here are some examples of package name expansion:
337              
338             @Service::Default MyApp::PluginBundle::Service::Default
339             *@Filter Brickyard::PluginBundle::Filter
340             *Filter Brickyard::Plugin::Filter
341             =Foo::Bar Foo::Bar
342             Some::Thing MyApp::Plugin::Some::Thing
343             -Thing::Frobnulizer MyApp::Role::Thing::Frobnulizer
344              
345             You can also define custom expansions. There are two ways to do this.
346             First you can pass a reference to an array of expansions to the
347             C method, or you can define them using the C key
348             in the configuration's root section. Each expansion is a string that
349             is evaluated for each package name. Custom expansions are useful if
350             you have plugins in several namespaces, for example.
351              
352             Here is an example of defining a custom expansion directly on the
353             L object:
354              
355             my $brickyard = Brickyard->new(
356             base_package => 'My::App',
357             expand => [ 's/^%/MyOtherApp::Plugin::/' ],
358             );
359              
360             Here is an example of defining it in the configuration's root section:
361              
362             expand = s/^%/MyOtherApp::Plugin::/
363              
364             [@Default]
365              
366             # this now refers to MyOtherApp::Plugin::Foo::Bar
367             [%Foo::Bar]
368             baz = 44
369              
370             =head2 init_from_config
371              
372             Takes a configuration file name specification or a reference to a
373             string containing the C string, a root object, and an optional
374             callback. The file specification can be a simple file name or
375             a colon-separated list of file names. Each of these files is
376             parsed with C and merged. The result is passed to
377             C, along with the root object and
378             optional callback - see its documentation for what these things do.
379              
380             When two configurations are merged, the root sections are merged like
381             a hash, but any plugin sections are appended in the order they are
382             found.
383              
384             This mechanism exists so you can, for example, have sensitive
385             information like passwords in a separate file. For example:
386              
387             $ cat myapp.ini
388             key1 = foo
389             key2.0 = bar0
390             key2.1 = bar1
391             [@Default]
392              
393             $ cat secret.ini
394             username = admin
395             password = mysecret
396             [Foo::Bar]
397              
398             To process both configuration files, use:
399              
400             $brickyard->init_from_config(
401             'myapp.ini:secret.ini', $root_config, $callback
402             );
403              
404             This is the same as having the following all-in-one configuration
405             file:
406              
407             key1 = foo
408             key2.0 = bar0
409             key2.1 = bar1
410             username = admin
411             password = mysecret
412              
413             [@Default]
414             [Foo::Bar]
415              
416             We use colons to separate configuration file names so it's easy to get
417             the specification from an environment variable.
418              
419             If the first argument is a scalar reference, it is assumed that it
420             refers to the C string. So you could pass the configuration
421             directly, without having a separate configuration file, like this:
422              
423             my $config = <
424             key1 = foo
425             key2.0 = bar0
426             key2.1 = bar1
427              
428             [@Default]
429             [Foo::Bar]
430             EOINI
431              
432             $brickyard->init_from_config(\$config, $root_config, $callback);
433              
434             =head2 init_from_config_structure
435              
436             Takes a configuration structure and a root object, and an optional
437             callback. For each configuration section it creates a plugin object,
438             initializes it with the plugin configuration hash and adds it to the
439             brickyard's array of plugins.
440              
441             Any configuration keys that appear in the configuration's root section
442             are set on the root object. So the root object can be anything that
443             has set-accessors for all the configuration keys that can appear in
444             the configuration's root section. One exception is the C key,
445             which is turned into a custom expansion; see above.
446              
447             The configuration needs to be a reference to a list of sections as
448             returned by C, for example.
449              
450             If an object is created that consumes the
451             L role, the bundle is processed
452             recursively.
453              
454             If the callback is given, each value from a key-value pair is filtered
455             through that callback. For example, you might want to support
456             environment variable expansion like this:
457              
458             $brickyard->init_from_config(
459             'myapp.ini',
460             $root_config,
461             sub {
462             my $value = shift;
463             $value =~ s/\$(\w+)/$ENV{$1} || "\$$1"/ge;
464             $value;
465             }
466             );
467              
468             =head2 plugins
469              
470             Read-write accessor for the reference to an array of plugins.
471              
472             =head2 plugins_with
473              
474             Takes a role name and returns a list of all the plugins that consume
475             this role. The result is cached, keyed by the role name.
476              
477             =head2 plugins_agree
478              
479             Takes a role name and a code reference and calls the code reference
480             once for each plugin that consumes the role. It returns 1 if the code
481             returns a true value for all plugins, 0 otherwise.
482              
483             An example will make this clearer:
484              
485             # Let the plugins decide
486             sub value_is_valid {
487             my ($self, $value) = @_;
488             $self->brickyard->plugins_agree(-ValueChecker =>
489             sub { $_->value_is_valid($value) }
490             }
491              
492             =head2 reset_plugins
493              
494             Clears the array of plugins as well as the cache - see
495             C.
496              
497             =head2 expand
498              
499             Holds custom package name expansions; see above.
500              
501             =head1 INSTALLATION
502              
503             See perlmodinstall for information and options on installing Perl modules.
504              
505             =head1 BUGS AND LIMITATIONS
506              
507             No bugs have been reported.
508              
509             Please report any bugs or feature requests through the web interface at
510             L.
511              
512             =head1 AVAILABILITY
513              
514             The latest version of this module is available from the Comprehensive Perl
515             Archive Network (CPAN). Visit L to find a CPAN
516             site near you, or see L.
517              
518             The development version lives at L
519             and may be cloned from L.
520             Instead of sending patches, please fork this project using the standard
521             git and github infrastructure.
522              
523             =head1 AUTHOR
524              
525             Marcel Gruenauer
526              
527             =head1 COPYRIGHT AND LICENSE
528              
529             This software is copyright (c) 2010 by Marcel Gruenauer.
530              
531             This is free software; you can redistribute it and/or modify it under
532             the same terms as the Perl 5 programming language system itself.