File Coverage

blib/lib/Hook/Modular.pm
Criterion Covered Total %
statement 171 239 71.5
branch 38 92 41.3
condition 7 30 23.3
subroutine 34 45 75.5
pod 25 25 100.0
total 275 431 63.8


line stmt bran cond sub pod time code
1 10     10   281336 use 5.008;
  10         118  
  10         402  
2 10     10   124 use strict;
  10         31  
  10         427  
3 10     10   52 use warnings;
  10         19  
  10         623  
4              
5             package Hook::Modular;
6             BEGIN {
7 10     10   180 $Hook::Modular::VERSION = '1.101050';
8             }
9             # ABSTRACT: Making pluggable applications easy
10 10     10   10955 use Encode ();
  10         1119025  
  10         266  
11 10     10   13210 use Data::Dumper;
  10         90044  
  10         6599  
12 10     10   10893 use File::Copy;
  10         31169  
  10         891  
13 10     10   68 use File::Spec;
  10         19  
  10         224  
14 10     10   55 use File::Basename;
  10         21  
  10         658  
15 10     10   11231 use File::Find::Rule (); # don't import rule()!
  10         104917  
  10         272  
16 10     10   8561 use Hook::Modular::ConfigLoader;
  10         122  
  10         202  
17 10     10   12309 use UNIVERSAL::require;
  10         9505  
  10         243  
18 10     10   316 use parent qw( Class::Accessor::Fast );
  10         20  
  10         107  
19             __PACKAGE__->mk_accessors(qw(conf plugins_path cache));
20 10     10   70450 use constant CACHE_CLASS => 'Hook::Modular::Cache';
  10         25  
  10         684  
21 10     10   52 use constant CACHE_PROXY_CLASS => 'Hook::Modular::CacheProxy';
  10         21  
  10         416  
22 10     10   50 use constant PLUGIN_NAMESPACE => 'Hook::Modular::Plugin';
  10         18  
  10         488  
23 10     10   472 use constant SHOULD_REWRITE_CONFIG => 0;
  10         63  
  10         134633  
24              
25             # Need an array, because rules live in Hook::Module::Rule::* as well as rule
26             # namespace of your subclassed program. We don't need such an array for
27             # PLUGIN_NAMESPACE because we don't have any plugins under
28             # 'Hook::Modular::Plugin::*'.
29             my @rule_namespaces = ('Hook::Modular::Rule');
30              
31             sub add_to_rule_namespaces {
32 3     3 1 8 my ($self, @ns) = @_;
33 3         9 push @rule_namespaces => @ns;
34             }
35              
36             sub rule_namespaces {
37 16 50   16 1 103 wantarray ? @rule_namespaces : \@rule_namespaces;
38             }
39             my $context;
40 47     47 1 328 sub context { $context }
41 12     12 1 35 sub set_context { $context = $_[1] }
42              
43             sub new {
44 12     12 1 58 my ($class, %opt) = @_;
45 12         91 my $self = bless {
46             conf => {},
47             plugins_path => {},
48             plugins => [],
49             rewrite_tasks => [],
50             }, $class;
51 12         145 my $loader = Hook::Modular::ConfigLoader->new;
52 12         81 my $config = $loader->load($opt{config}, $self);
53 12         87 $loader->load_include($config);
54 12         75 $self->{conf} = $config->{global};
55 12   50     112 $self->{conf}{log} ||= { level => 'debug' };
56 12   33     137 $self->{conf}{plugin_namespace} ||= $self->PLUGIN_NAMESPACE;
57              
58             # don't use ||= here, as we are dealing with boolean values, so "0" is a
59             # possible value.
60 12 100       52 unless (defined $self->{conf}{should_rewrite_config}) {
61 10         85 $self->{conf}{should_rewrite_config} = $self->SHOULD_REWRITE_CONFIG;
62             }
63 12 100       88 if (my $ns = $self->{conf}{rule_namespaces}) {
64 3 50       15 $ns = [$ns] unless ref $ns eq 'ARRAY';
65 3         25 $self->add_to_rule_namespaces(@$ns);
66             }
67 12 50       28 if (eval { require Term::Encoding }) {
  12         10374  
68 12   33     9423 $self->{conf}{log}{encoding} ||= Term::Encoding::get_encoding();
69             }
70 12         30000 Hook::Modular->set_context($self);
71 12         149 $loader->load_recipes($config);
72 12         166 $self->load_cache($opt{config});
73 12 50       109 $self->load_plugins(@{ $config->{plugins} || [] });
  12         286  
74 3         25 $self->rewrite_config
75 12 100 66     106 if $self->{conf}{should_rewrite_config} && @{ $self->{rewrite_tasks} };
76              
77             # for subclasses
78 12         92 $self->init;
79 12         215 $self;
80             }
81 12     12 1 21 sub init { }
82              
83             sub bootstrap {
84 12     12   14931 my $class = shift;
85 12         100 my $self = $class->new(@_);
86 12         76 $self->run;
87 12         42547 $self;
88             }
89              
90             sub add_rewrite_task {
91 3     3 1 8 my ($self, @stuff) = @_;
92 3         5 push @{ $self->{rewrite_tasks} }, \@stuff;
  3         14  
93             }
94              
95             sub rewrite_config {
96 3     3 1 4 my $self = shift;
97 3 100       9 unless ($self->{config_path}) {
98 2         7 $self->log(
99             warn => "config is not loaded from file. Ignoring rewrite tasks.");
100 2         16 $self->{trace}{ignored_rewrite_config}++; # for tests
101 2         4 return;
102             }
103 1 50       42 open my $fh, '<', $self->{config_path}
104             or $self->error("$self->{config_path}: $!");
105 1         36 my $data = join '', <$fh>;
106 1         19 close $fh;
107 1         2 my $count;
108              
109             # xxx this is a quick hack: It should be a YAML roundtrip maybe
110 1         2 for my $task (@{ $self->{rewrite_tasks} }) {
  1         3  
111 1         2 my ($key, $old_value, $new_value) = @$task;
112 1 50       56 if ($data =~ s/^(\s+$key:\s+)\Q$old_value\E[ \t]*$/$1$new_value/m) {
113 1         4 $count++;
114             } else {
115 0         0 $self->log(
116             error => "$key: $old_value not found in $self->{config_path}");
117             }
118             }
119 1 50       4 if ($count) {
120 1         9 File::Copy::copy($self->{config_path}, $self->{config_path} . '.bak');
121 1 50       602 open my $fh, '>', $self->{config_path}
122             or return $self->log(error => "$self->{config_path}: $!");
123 1         5 print $fh $data;
124 1         32 close $fh;
125 1         10 $self->log(info =>
126             "Rewrote $count password(s) and saved to $self->{config_path}");
127             }
128             }
129              
130             sub load_cache {
131 12     12 1 32 my ($self, $config) = @_;
132              
133             # cache is auto-vivified but that's okay
134 12 50       99 unless ($self->{conf}{cache}{base}) {
135              
136             # use config filename as a base directory for cache
137 0   0     0 my $base = (basename($config) =~ /^(.*?)\.yaml$/)[0] || 'config';
138 0 0       0 my $dir = $base eq 'config' ? ".$0" : ".$0-$base";
139 0   0     0 $self->{conf}{cache}{base} ||=
140             File::Spec->catfile($self->home_dir, $dir);
141             }
142 12         237 my $cache_class = $self->CACHE_CLASS;
143 12 50       125 $cache_class->require or die $@;
144 12         5600 $self->cache($cache_class->new($self->{conf}{cache}));
145             }
146              
147             sub home_dir {
148 0     0 1 0 eval { require File::HomeDir };
  0         0  
149 0 0       0 return $@ ? $ENV{HOME} : File::HomeDir->my_home;
150             }
151              
152             sub load_plugins {
153 12     12 1 38 my ($self, @plugins) = @_;
154 12   50     52 my $plugin_path = $self->conf->{plugin_path} || [];
155 12 50       193 $plugin_path = [$plugin_path] unless ref $plugin_path;
156 12         41 for my $path (@$plugin_path) {
157 0 0       0 opendir my $dir, $path or do {
158 0         0 $self->log(warn => "$path: $!");
159 0         0 next;
160             };
161 0         0 while (my $ent = readdir $dir) {
162 0 0       0 next if $ent =~ /^\./;
163 0         0 $ent = File::Spec->catfile($path, $ent);
164 0 0 0     0 if (-f $ent && $ent =~ /\.pm$/) {
    0          
165 0         0 $self->add_plugin_path($ent);
166             } elsif (-d $ent) {
167 0         0 my $lib = File::Spec->catfile($ent, "lib");
168 0 0 0     0 if (-e $lib && -d _) {
169 0         0 $self->log(debug => "Add $lib to INC path");
170 0         0 unshift @INC, $lib;
171             } else {
172 0         0 my $rule = File::Find::Rule->new;
173 0         0 $rule->file;
174 0         0 $rule->name('*.pm');
175 0         0 my @modules = $rule->in($ent);
176 0         0 for my $module (@modules) {
177 0         0 $self->add_plugin_path($module);
178             }
179             }
180             }
181             }
182             }
183 12         32 for my $plugin (@plugins) {
184 16 50       182 $self->load_plugin($plugin) unless $plugin->{disable};
185             }
186             }
187              
188             sub add_plugin_path {
189 0     0 1 0 my ($self, $file) = @_;
190 0 0       0 my $pkg = $self->extract_package($file)
191             or die "Can't find package from $file";
192 0         0 $self->plugins_path->{$pkg} = $file;
193 0         0 $self->log(debug => "$file is added as a path to plugin $pkg");
194             }
195              
196             sub extract_package {
197 0     0 1 0 my ($self, $file) = @_;
198 0         0 my $ns = $self->{conf}{plugin_namespace} . '::';
199 0 0       0 open my $fh, '<', $file or die "$file: $!";
200 0         0 while (<$fh>) {
201 0 0       0 /^package ($ns.*?);/ and return $1;
202             }
203 0         0 return;
204             }
205              
206             sub autoload_plugin {
207 0     0 1 0 my ($self, $plugin) = @_;
208 0 0       0 unless ($self->is_loaded($plugin->{module})) {
209 0         0 $self->load_plugin($plugin);
210             }
211             }
212              
213             sub is_loaded {
214 0     0 1 0 my ($self, $stuff) = @_;
215             my $sub =
216             ref $stuff && ref $stuff eq 'Regexp'
217 0     0   0 ? sub { $_[0] =~ $stuff }
218 0 0 0 0   0 : sub { $_[0] eq $stuff };
  0         0  
219 0         0 my $ns = $self->{conf}{plugin_namespace} . '::';
220 0         0 for my $plugin (@{ $self->{plugins} }) {
  0         0  
221 0         0 my $module = ref $plugin;
222 0         0 $module =~ s/^$ns//;
223 0 0       0 return 1 if $sub->($module);
224             }
225 0         0 return;
226             }
227              
228             sub load_plugin {
229 16     16 1 38 my ($self, $config) = @_;
230 16         65 my $ns = $self->{conf}{plugin_namespace} . '::';
231 16         59 my $module = delete $config->{module};
232 16 50       94 if ($module !~ s/^\+//) {
233 16         201 $module =~ s/^$ns//;
234 16         55 $module = $ns . $module;
235             }
236 16 50       276 if ($module->isa($self->{conf}{plugin_namespace})) {
    50          
237 0         0 $self->log(debug => "$module is loaded elsewhere ... maybe .t script?");
238             } elsif (my $path = $self->plugins_path->{$module}) {
239 0 0       0 $path->require or die $@;
240             } else {
241 16 50       195 $module->require or die $@;
242             }
243 16         5090 $self->log(info => "plugin $module loaded.");
244 16         337 my $plugin = $module->new($config);
245 16         114 my $cache_proxy_class = $self->CACHE_PROXY_CLASS;
246 16 50       116 $cache_proxy_class->require or die $@;
247 16         398 $plugin->cache($cache_proxy_class->new($plugin, $self->cache));
248 16         150 $plugin->register($self);
249 16         25 push @{ $self->{plugins} }, $plugin;
  16         212  
250             }
251              
252             sub register_hook {
253 16     16 1 254 my ($self, $plugin, @hooks) = @_;
254 16         115 while (my ($hook, $callback) = splice @hooks, 0, 2) {
255              
256             # set default rule_hook $hook to $plugin
257 27 100       138 $plugin->rule_hook($hook) unless $plugin->rule_hook;
258 27         348 push @{ $self->{hooks}{$hook} },
  27         231  
259             +{callback => $callback,
260             plugin => $plugin,
261             };
262             }
263             }
264              
265             sub run_hook {
266 13     13 1 127 my ($self, $hook, $args, $once, $callback) = @_;
267 13         21 my @ret;
268 13         64 $self->log(debug => "run_hook $hook");
269 13         115 for my $action (@{ $self->{hooks}{$hook} }) {
  13         47  
270 17         31 my $plugin = $action->{plugin};
271 17         88 $self->log(debug => sprintf('--> plugin %s', ref $plugin));
272 17 100       240 if ($plugin->rule->dispatch($plugin, $hook, $args)) {
273 12         33 $self->log(debug => "----> running action");
274 12         119 my $ret = $action->{callback}->($plugin, $self, $args);
275 12 50       125 $callback->($ret) if $callback;
276 12 50       65 if ($once) {
277 0 0       0 return $ret if defined $ret;
278             } else {
279 12         43 push @ret, $ret;
280             }
281             } else {
282 5         24 push @ret, undef;
283             }
284             }
285 13 50       40 return if $once;
286 13         41 return @ret;
287             }
288              
289             sub run_hook_once {
290 0     0 1 0 my ($self, $hook, $args, $callback) = @_;
291 0         0 $self->run_hook($hook, $args, 1, $callback);
292             }
293              
294             sub run_main {
295 0     0 1 0 my $self = shift;
296 0         0 $self->run_hook('plugin.init');
297 0         0 $self->run;
298 0         0 $self->run_hook('plugin.finalize');
299 0         0 Hook::Modular->set_context(undef);
300 0         0 $self;
301             }
302 12     12 1 113 sub run { }
303              
304             sub log {
305 73     73 1 200 my ($self, $level, $msg, %opt) = @_;
306 73 100       284 return unless $self->should_log($level);
307              
308             # hack to get the original caller as Plugin or Rule
309 12         208 my $caller = $opt{caller};
310 12 50       50 unless ($caller) {
311 12         25 my $i = 0;
312 12         79 while (my $c = caller($i++)) {
313 12 50       106 last if $c !~ /Plugin|Rule/;
314 0         0 $caller = $c;
315             }
316 12   33     95 $caller ||= caller(0);
317             }
318 12         44 chomp($msg);
319 12 50       71 if ($self->conf->{log}->{encoding}) {
320 12 50       482 $msg = Encode::decode_utf8($msg) unless utf8::is_utf8($msg);
321 12         1078 $msg = Encode::encode($self->conf->{log}->{encoding}, $msg);
322             }
323 12         9006 warn "$caller [$level] $msg\n";
324             }
325             my %levels = (
326             debug => 0,
327             warn => 1,
328             info => 2,
329             error => 3,
330             );
331              
332             sub should_log {
333 73     73 1 127 my ($self, $level) = @_;
334 73         445 $levels{$level} >= $levels{ $self->conf->{log}->{level} };
335             }
336              
337             sub error {
338 0     0 1   my ($self, $msg) = @_;
339 0           my ($caller, $filename, $line) = caller(0);
340 0           chomp($msg);
341 0           die "$caller [fatal] $msg at file $filename line $line\n";
342             }
343              
344             sub dumper {
345 0     0 1   my ($self, $stuff) = @_;
346 0           local $Data::Dumper::Indent = 1;
347 0           $self->log(debug => Dumper $stuff);
348             }
349             1;
350              
351              
352             =pod
353              
354             =for stopwords conf
355              
356             =for test_synopsis 1;
357             __END__