File Coverage

blib/lib/MooX/Object/Pluggable.pm
Criterion Covered Total %
statement 88 92 95.6
branch 24 26 92.3
condition 3 3 100.0
subroutine 15 17 88.2
pod 3 5 60.0
total 133 143 93.0


line stmt bran cond sub pod time code
1             package MooX::Object::Pluggable;
2 4     4   571660 use Moo::Role;
  4         22039  
  4         25  
3 4     4   1478 use Modern::Perl;
  4         18  
  4         29  
4 4     4   496 use Scalar::Util 'refaddr';
  4         5  
  4         333  
5             require Module::Pluggable::Object;
6 4     4   944 use namespace::clean;
  4         16287  
  4         24  
7              
8             our $VERSION = '0.0.4'; # VERSION
9             # ABSTRACT: Moo eXtension to inject plugins to exist objects as a role
10              
11              
12             sub _apply_roles {
13 16     16   26 my ($self, @roles) = @_;
14 21         122 map {
15 16         17 my $role = $_;
16 21 100       68 Moo::Role->apply_roles_to_object($self, $role) unless $self->does($role)
17             } @roles;
18 16         5434 return $self;
19             }
20              
21 0     0 0 0 sub load_plugin { load_plugins(@_) }
22              
23             sub load_plugins {
24 15     15 1 455 my ($self, @plugin_options) = @_;
25 15         37 my $pluggable_object = $self->_pluggable_object;
26 15         90 my @plugins = $pluggable_object->plugins;
27             # Provide ability for roles in a real package, with syntax: '+MooX::ConfigFromFile'
28 1         2 map {
29 1         3 my $option = $_; $option=~s/^\+//;
  17         85  
30 1         2 $self->_apply_roles($option);
31 15         7070 } grep { /^\+/ } @plugin_options;
32 15 50       38 return $self unless @plugins;
33 15         22 for my $plugin_option (@plugin_options) {
34 17 100       71 if ($plugin_option eq '-all') {
    100          
    100          
35 1         2 $self->_apply_roles(@plugins);
36             } elsif (ref $plugin_option eq 'ARRAY') {
37 1         5 $self->load_plugins(@$plugin_option);
38             } elsif (ref $plugin_option eq 'Regexp') {
39 4         6 my @load_plugins = grep { $plugin_option } @plugins;
  8         9  
40 4 50       6 return $self unless @load_plugins;
41 4         11 $self->_apply_roles(@load_plugins);
42             } else {
43 11         12 my @load_plugins = map { $_.'::'.$plugin_option } @{$pluggable_object->{search_path}};
  11         31  
  11         22  
44 11         16 my %all_plugins = map { $_ => 1 } @plugins;
  17         31  
45 11         14 my @real_roles = grep { $all_plugins{$_} } @load_plugins;
  11         20  
46 11 100       25 return $self unless @real_roles;
47 10         30 $self->_apply_roles(@real_roles)
48             }
49             }
50 14         94 return $self;
51             }
52              
53              
54             sub plugins {
55 1     1 1 2 my ($self) = @_;
56 1         2 $self->_pluggable_object->plugins;
57             }
58              
59              
60             sub loaded_plugins {
61 0     0 1 0 my $self = shift;
62 0         0 grep { $self->does($_) } $self->plugins;
  0         0  
63             }
64              
65              
66             my %pluggable_objects = (); # key: object, value: loaded plugins
67              
68 13     13 0 12432 sub BUILD { } # BUILD() will be override by consumers, so we use afterBuild
69              
70             after BUILD => sub {
71             my ($self, $opts) = @_;
72             if (defined $opts->{pluggable_options}) {
73             my $pluggable_options = $opts->{pluggable_options};
74             $pluggable_options->{package} = ref $self ? ref $self : $self;
75             $pluggable_objects{refaddr($self)} = Module::Pluggable::Object->new(%$pluggable_options);
76             }
77             if (defined $self->_build_load_plugins and scalar @{$self->_build_load_plugins} > 0) {
78             $self->load_plugins(@{$self->_build_load_plugins});
79             }
80             if (defined $opts->{load_plugins}) {
81             $self->load_plugins(ref $opts->{load_plugins} eq 'ARRAY' ?
82             @{$opts->{load_plugins}} : $opts->{load_plugins}
83             );
84             }
85             };
86              
87 2     2   4 sub _build_pluggable_options { {} }
88              
89 18     18   42 sub _build_load_plugins { [] }
90              
91             sub _pluggable_object {
92 28     28   11061 my $self = shift;
93 28         20 my ($class, $addr);
94 28 100       50 if (ref $self) {
95 23         43 $class = ref $self;
96 23         56 $addr = refaddr $self;
97             } else {
98 5         6 $class = $self;
99             }
100             # Find self pluggable object;
101 28 100 100     108 return $pluggable_objects{$addr} if defined $addr and defined $pluggable_objects{$addr};
102             # Find package pluggable object;
103 25         44 $class=~s/__WITH__.*//g; # use parent package name as class name.
104 25 100       67 return $pluggable_objects{$class} if defined $pluggable_objects{$class};
105             # Not found, create a new one for package.
106 5         87 my $pluggable_options = $self->_build_pluggable_options;
107 5         14 $pluggable_options->{package} = $class;
108 5         52 $pluggable_objects{$class} = Module::Pluggable::Object->new(
109             %$pluggable_options,
110             );
111             }
112              
113              
114             sub _inject_roles_to {
115 8     8   13 my ($target, $import_options) = @_;
116 8         84 my $with = $target->can("with");
117 8 100       29 return unless $with; # Do nothing unless it's a Moo(se) object or role.
118              
119 5         19 $with->('MooX::Object::Pluggable');
120 5         16029 my $around = $target->can("around");
121 5         31 for my $builder (qw/pluggable_options load_plugins/) {
122 10         2097 my ($key) = grep /$builder/, keys %$import_options;
123 10 100       28 next unless $key;
124 7     18   42 $around->("_build_$builder" => sub { $import_options->{$key} });
  18         477  
125             }
126             }
127              
128             sub import
129             {
130 7     7   31714 my ( undef, %import_options ) = @_;
131 7         21 my $target = caller;
132             # Inject roles to target namespace
133 7         24 &_inject_roles_to($target, \%import_options);
134              
135             # Compatible for MooX
136 7         899 my $around = $target->can("around");
137 7 100       1844 return unless $around;
138             $around->("import" => sub {
139 1     1   64 my ($orig, $self, @opts) = @_;
140 1         3 my %pluggable_opts = map { $opts[$_] => $opts[$_ + 1] } grep { $opts[$_] =~/^-(pluggable_options|load_plugins)$/ } 0..$#opts;
  1         5  
  2         11  
141 1         4 &_inject_roles_to($target, \%pluggable_opts);
142 1         296 my %hash = map { $_ => 1 } %pluggable_opts;
  2         6  
143 1         3 my @remains = grep { ! defined $hash{$_} } @opts;
  2         6  
144 1         4 $self->$orig(@remains);
145 4         34 });
146 4         1316 return;
147             }
148              
149              
150             1;
151              
152             __END__
153              
154             =pod
155              
156             =encoding UTF-8
157              
158             =head1 NAME
159              
160             MooX::Object::Pluggable - Moo eXtension to inject plugins to exist objects as a role
161              
162             =head1 VERSION
163              
164             version 0.0.4
165              
166             =head1 SYNOPSIS
167              
168             In your package:
169              
170             package MyPackage;
171             use Moo;
172             use namespace::clean;
173              
174             with 'MooX::Object::Pluggable';
175             1
176              
177             Define your plugin package:
178              
179             package MyPackage::Plugin::Foo;
180             use Moo::Role;
181             use namespace::clean;
182              
183             sub foo { 'foo' }
184              
185             Then in your script:
186              
187             #!perl
188             use MyPackage;
189             my $object = MyPackage->new;
190             $object->load_plugins('Foo');
191              
192             Or C<new> with pluggable options:
193              
194             use MyPackage;
195             MyPackage->new(
196             pluggable_options => { search_path => 'MyPackage::Plugin' }, # optional
197             load_plugins => [ "Foo", qr/::Bar$/ ]
198             );
199              
200             Or use MooX with this:
201              
202             use MooX 'Object::Pluggable' => { ... };
203              
204             =head1 DESCRIPTION
205              
206             C<MooX::Object::Pluggable> for moo is designed to perform like C<MooseX::Object::Pluggable>
207             for Moose staff. Mainly it use Moo::Role's C<apply_roles_to_object> to load plugins
208             at runtime, but with the ability to choose plugins with package L<Module::Pluggable::Object>.
209              
210             =head1 METHODS
211              
212             =head2 load_plugins
213              
214             In most situation, your need only call the fuction C<load_plugins> on an object.
215             The parameters support String, Regexp, or Array or ArrayRef of them.
216              
217             eg.
218              
219             $o->load_plugins("Foo", "Bar", qr/^Class::Plugin::(Abc|N)[0-9]/, [ qw/Other Way/ ]);
220              
221             And there's another syntax sugar, when you just want to load a specific role:
222              
223             $o->load_plugins("+MooX::ConfigFromFile::Role");
224             # Notice that the '+' sign does not support Regexp, use whole package name with it.
225              
226             =head2 plugins
227              
228             The method C<plugins> returns a array of plugins, defaultly in the namespace
229             C<Your::Package::Plugin::>. You can manage it by implement the C<_build_pluggable_options>
230             in your package and given the avaliable options' HashRef.
231              
232             package MyPackage;
233             use Moo;
234             with 'MooX::Object::Pluggable';
235             sub _build_pluggable_options {
236             { search_path => __PACKAGE__.'::Funtionals' }
237             }
238              
239             All the avaliable options will be found in tutorial of package L<Module::Pluggable>.
240              
241             =head2 loaded_plugins
242              
243             This will list all loaded plugins of current object for you.
244              
245             =head1 DESIGN
246              
247             Considering not import any new attributes to the consumers,
248             I'm using a private variable for help to maintain L<Module::Pluggable::Object>
249             objects so that it only create once for each package,
250             and could provide private configuration for specific objects
251             that use diffent pluggable options in C<new>.
252              
253             There's two way to configure user defined pluggable options.
254              
255             =head2 new(pluggable_options => {}, load_plugins => [])
256              
257             User could directly use there specific options for plugin.
258             And create objects with some plugins after C<BUILD> step.
259              
260             =head2 _build_pluggable_options
261              
262             Implement this build function in your package, and C<MooX::Object::Pluggable>
263             will apply the options for you.
264              
265             And you still could change default options in C<new> method.
266              
267             =head1 MooX
268              
269             A L<MooX>-compatible interface like this:
270              
271             package MyPackage::Hello;
272             use Moo::Role;
273             sub hello { 'hello' }
274              
275             ...
276              
277             package MyPackage;
278             use MooX::Object::Pluggable -pluggable_options => { search_path => ["MyPackage"] }, -load_plugins => ['Hello'];
279              
280             Or:
281              
282             use MooX
283             'Object::Pluggable' => { -pluggable_options => { search_path => ["MyPackage"] }, -load_plugins => ['Hello'] };
284              
285             =head1 SEE ALSO
286              
287             L<Module::Pluggable>, L<MooseX::Object::Pluggable>
288              
289             =head1 AUTHOR
290              
291             Huo Linhe <huolinhe@berrygenomics.com>
292              
293             =head1 COPYRIGHT AND LICENSE
294              
295             This software is copyright (c) 2015 by Berry Genomics.
296              
297             This is free software; you can redistribute it and/or modify it under
298             the same terms as the Perl 5 programming language system itself.
299              
300             =cut