File Coverage

blib/lib/Brickyard.pm
Criterion Covered Total %
statement 126 133 94.7
branch 46 58 79.3
condition 4 4 100.0
subroutine 17 18 94.4
pod 8 8 100.0
total 201 221 90.9


line stmt bran cond sub pod time code
1 4     4   121001 use 5.010;
  4         13  
  4         162  
2 4     4   22 use warnings;
  4         6  
  4         111  
3 4     4   17 use strict;
  4         7  
  4         183  
4              
5             package Brickyard;
6             BEGIN {
7 4     4   94 $Brickyard::VERSION = '1.111750';
8             }
9              
10             # ABSTRACT: Plugin system based on roles
11 4         24 use Brickyard::Accessor rw =>
12 4     4   2047 [qw(base_package expand plugins plugins_role_cache)];
  4         10  
13 4     4   17 use Carp qw(croak);
  4         7  
  4         13076  
14              
15             sub new {
16 15     15 1 24785 my $class = shift;
17 15         143 bless {
18             base_package => 'MyApp',
19             expand => [],
20             plugins => [],
21             plugins_role_cache => {},
22             @_
23             }, $class;
24             }
25              
26             sub plugins_with {
27 4     4 1 27 my ($self, $role) = @_;
28 4         12 $role = $self->expand_package($role);
29 6         49 $self->plugins_role_cache->{$role} ||=
30 4   100     17 [ grep { $_->DOES($role) } @{ $self->plugins } ];
  2         9  
31 4         28 @{ $self->plugins_role_cache->{$role} };
  4         15  
32             }
33              
34             sub plugins_agree {
35 0     0 1 0 my ($self, $role, $code) = @_;
36 0         0 my @plugins = $self->plugins_with($role);
37 0 0       0 return unless @plugins;
38 0         0 for (@plugins) {
39              
40             # $code can use $_->foo($bar)
41 0 0       0 return unless $code->();
42             }
43 0         0 1;
44             }
45              
46             sub reset_plugins {
47 1     1 1 1186 my $self = shift;
48 1         6 $self->plugins([]);
49 1         4 $self->plugins_role_cache({});
50             }
51              
52             sub parse_ini {
53 7     7 1 16 my ($self, $ini, $callback) = @_;
54 7   100 20   74 $callback //= sub { $_[0] }; # default: identity function
  20         42  
55 7         25 my @result = ([ '_', '_', {} ]);
56 7         17 my $counter = 0;
57 7         187 foreach (split /(?:\015{1,2}\012|\015|\012)/, $ini) {
58 47         56 $counter++;
59 47 100       160 next if /^\s*(?:\#|\;|$)/; # Skip comments and empty lines
60 34         58 s/\s\;\s.+$//g; # Remove inline comments
61              
62             # Handle section headers
63 34 100       114 if (/^\s*\[\s*(.+?)\s*\]\s*$/) {
64 10         44 push @result, [ $1, $1, {} ];
65 10         22 next;
66             }
67              
68             # Handle properties
69 24 50       206 if (/^\s*([^=]+?)\s*=\s*(.*?)\s*$/) {
70 24         68 my ($key, $value) = ($1, $2);
71 24         52 $value = $callback->($value);
72 24         62 my $section = $result[-1][2];
73              
74             # if a property is seen multiple times, it becomes an array
75 24 100       60 if (exists $section->{$key}) {
76 6 100       32 $section->{$key} = [ $section->{$key} ]
77             unless ref $section->{$key} eq 'ARRAY';
78 6         8 push @{ $section->{$key} } => $value;
  6         18  
79             } else {
80 18         47 $section->{$key} = $value;
81             }
82 24         102 next;
83             }
84 0         0 die "Syntax error in INI file at line $counter: '$_'";
85             }
86 7         120 \@result;
87             }
88              
89             # appropriated from CGI::Expand
90             sub _expand_hash {
91 17     17   29 my $flat = $_[1];
92 17         27 my $deep = {};
93 17         72 for my $name (keys %$flat) {
94 32         117 my ($first, @segments) = split /\./, $name;
95 32         65 my $box_ref = \$deep->{$first};
96 32         57 for (@segments) {
97 28 100       96 if (/^(0|[1-9]\d*)$/) {
98 11 100       36 $$box_ref = [] unless defined $$box_ref;
99 11 100       57 croak "param clash for $name($_)"
100             unless ref $$box_ref eq 'ARRAY';
101 10         37 $box_ref = \($$box_ref->[$1]);
102             } else {
103 17 100       50 $$box_ref = {} unless defined $$box_ref;
104 17 50       48 croak "param clash for $name($_)"
105             unless ref $$box_ref eq 'HASH';
106 17         54 $box_ref = \($$box_ref->{$_});
107             }
108             }
109 31 50       72 croak "param clash for $name value $flat->{$name}"
110             if defined $$box_ref;
111 31         81 $$box_ref = $flat->{$name};
112             }
113 16         63 $deep;
114             }
115              
116             sub expand_package {
117 31     31 1 54 my $self = shift;
118 31         55 local $_ = shift;
119 31 100       172 my $base = s/^\*// ? 'Brickyard' : $self->base_package;
120 31 100       158 return $_ if s/^@(?=\w)/$base\::PluginBundle::/;
121 24 100       118 return $_ if s/^-(?=\w)/$base\::Role::/;
122 19 100       54 return $_ if s/^=(?=\w)//;
123 18         27 for my $expand (@{ $self->expand }) {
  18         58  
124 11         22 my $before = $_;
125 11         836 eval $expand;
126 11 100       58 return $_ if $_ ne $before;
127 8 50       31 die $@ if $@;
128             }
129 15         77 "$base\::Plugin::$_";
130             }
131              
132             sub _read_config_file {
133 4     4   10 my ($self, $file) = @_;
134 4 50       228 open my $fh, '<', $file or die "can't open $file for reading: $!\n";
135 4         7 my $config = do { local $/; <$fh> };
  4         18  
  4         155  
136 4 50       66 close $fh or die "can't close $file: $!\n";
137 4         30 $config;
138             }
139              
140             sub _merge_configs {
141 4     4   8 my ($self, $merged_config, $new_config) = @_;
142 4 100       18 return $new_config unless ref $merged_config eq 'ARRAY';
143 1         3 for my $new_section (@$new_config) {
144 2         5 my ($local_name, $plugin_config) = @{$new_section}[0,2];
  2         6  
145 2 100       6 if ($local_name eq '_') {
146             # assume the merged config's root section is at the start
147 1         8 $merged_config->[0][2] = {
148 1         2 %{ $merged_config->[0][2] },
149             %$plugin_config
150             };
151             } else {
152 1         3 push @$merged_config => $new_section;
153             }
154             }
155 1         5 $merged_config;
156             }
157              
158             sub init_from_config {
159 4     4 1 82 my ($self, $config, $root, $callback) = @_;
160 4 100       20 if (ref $config eq 'SCALAR') {
161             # $config is a reference to the INI string
162 1         6 my $this_config = $self->parse_ini($$config, $callback);
163 1         6 $_->[2] = $self->_expand_hash($_->[2]) for @$this_config;
164 1         5 $self->init_from_config_structure($this_config, $root, $callback);
165             } else {
166             # $config is a filename
167 3         6 my $merged_config;
168 3         21 my @files = split /:/ => $config;
169 3         11 for my $file (@files) {
170 4         39 my $this_config = $self->parse_ini($self->_read_config_file($file), $callback);
171 4         22 $_->[2] = $self->_expand_hash($_->[2]) for @$this_config;
172 4         17 $merged_config = $self->_merge_configs($merged_config, $this_config);
173             }
174 3         14 $self->init_from_config_structure($merged_config, $root, $callback);
175             }
176             }
177              
178             sub init_from_config_structure {
179 8     8 1 57 my ($self, $config, $root, $callback) = @_;
180 8         20 for my $section (@$config) {
181 21         51 my ($local_name, $name, $plugin_config) = @$section;
182 21 100       51 if ($local_name eq '_') {
183              
184             # Global container configuration
185 4         23 while (my ($key, $value) = each %$plugin_config) {
186 6 100       15 if ($key eq 'expand') {
187 2 50       4 push @{ $self->expand },
  2         14  
188             ref $value eq 'ARRAY' ? @$value : $value;
189             } else {
190 4         20 $root->$key($value);
191             }
192             }
193             } else {
194 17         55 my $package = $section->[1] = $self->expand_package($name);
195 17         1104 eval "require $package";
196 17 50       23661 die "Cannot require $package: $@" if $@;
197 17 100       68 if ($package->DOES('Brickyard::Role::PluginBundle')) {
198 4         62 my $bundle = $package->new(brickyard => $self, %$plugin_config);
199 4         18 $self->init_from_config_structure($bundle->bundle_config, $root);
200             } else {
201 13         118 push @{ $self->plugins } => $package->new(
  13         47  
202             name => $local_name,
203             brickyard => $self,
204             %$plugin_config
205             );
206             }
207             }
208             }
209             }
210             1;
211              
212              
213             __END__