File Coverage

blib/lib/Package/Butcher.pm
Criterion Covered Total %
statement 135 147 91.8
branch 20 42 47.6
condition 1 3 33.3
subroutine 28 29 96.5
pod 3 3 100.0
total 187 224 83.4


line stmt bran cond sub pod time code
1             package Package::Butcher;
2              
3 2     2   50566 use warnings;
  2         4  
  2         66  
4 2     2   9 use strict;
  2         4  
  2         56  
5              
6 2     2   1152 use Package::Butcher::Inflator;
  2         4  
  2         44  
7 2     2   11 use Carp ();
  2         2  
  2         200  
8              
9 2     2   10 use constant VALID_PACKAGE_RE => qr/^\w+(?:::\w+)*$/;
  2         4  
  2         285  
10 2     2   9 use constant VALID_SUBROUTINE_RE => qr/^[_[:alpha:]][[:word:]]*$/;
  2         3  
  2         312  
11              
12             our $VERSION = '0.02';
13              
14             sub new {
15 1     1 1 40 my ( $class, $arg_for ) = @_;
16 1         6 my $self = bless {} => $class;
17 1         4 return $self->_initialize($arg_for);
18             }
19              
20             sub _initialize {
21 1     1   2 my ( $self, $arg_for ) = @_;
22 1         10 my %default_for = (
23             package => delete $arg_for->{package},
24             import_on_use => delete $arg_for->{import_on_use},
25             is_package_loaded => 0,
26             subs_installed => {},
27             );
28 1         7 foreach my $method ( keys %default_for ) {
29 4         22 $self->{$method} = $default_for{$method};
30 2     2   17 no strict 'refs';
  2         12  
  2         936  
31 4     8   27 *$method = sub { $_[0]->{$method} };
  8         40  
32             }
33 1         6 $self->_do_not_load( delete $arg_for->{do_not_load} );
34             # _sub() must be called before _predeclare
35 1         7 $self->_subs( delete $arg_for->{subs} );
36 1         7 $self->_predeclare( delete $arg_for->{predeclare} );
37 1         6 $self->_method_chains( delete $arg_for->{method_chains} );
38              
39 1         6 return $self;
40             }
41              
42 1     1   9 sub _is_package_loaded { $_[0]->{is_package_loaded} = $_[1] }
43              
44             sub _assert_looks_like_package {
45 4     4   8 my ( $proto, $package ) = @_;
46 4 50       36 unless ( $package =~ VALID_PACKAGE_RE ) {
47 0         0 Carp::confess(
48             "'$package' does not look like a valid package name to me");
49             }
50             }
51              
52             sub _assert_looks_like_subroutine {
53 8     8   12 my ( $proto, $subroutine ) = @_;
54 8 50       36 unless ( $subroutine =~ VALID_SUBROUTINE_RE ) {
55 0         0 Carp::confess("'$subroutine' does not look like a valid subroutine name to me");
56             }
57             }
58              
59             sub _do_not_load {
60 1     1   3 my ( $self, $packages ) = @_;
61 1 50       4 return unless $packages;
62 1 50       6 $packages = [$packages] unless 'ARRAY' eq ref $packages;
63 1         25 foreach my $package (@$packages) {
64 3         12 $self->_assert_looks_like_package($package);
65 3         9 my $file = "$package.pm";
66 3         13 $file =~ s{::}{/}g;
67 3         7 my $butcher = ref $self;
68 3         8 my $message = "loaded via '$butcher'";
69 3 50 33     16 if ( $INC{$file} && $INC{$file} ne $message ) {
70 0         0 Carp::cluck("'$package' already loaded via '$INC{$file}'");
71             }
72             else {
73 3         8 $INC{$file} = $message;
74              
75             # This ensures that "use Foo 'bar'" won't generate "package Foo
76             # doesn't export bar" errors
77 2     2   12 no strict 'refs';
  2         3  
  2         839  
78 3     3   28 *{"${package}::import"} = sub {};
  3         687  
  3         74  
79             }
80             }
81             }
82              
83             sub _subs {
84 1     1   2 my ( $self, $subs ) = @_;
85 1 50       4 return unless $subs;
86 1         3 my $subs_installed = $self->subs_installed;
87 1         5 foreach my $sub (keys %$subs) {
88 3         12 $self->_assert_looks_like_subroutine($sub);
89 3 50       9 if (exists $subs_installed->{$sub} ) {
90 0         0 Carp::confess("Cannot install a subroutine already installed: '$sub'");
91             }
92 3         4 my $code = $subs->{$sub};
93 3 50       10 unless ( 'CODE' eq ref $code ) {
94 0         0 Carp::confess("The value for '$sub' must be a subroutine reference");
95             }
96 3         7 $subs_installed->{$sub} = $code;
97             }
98             }
99              
100             sub _method_chains {
101 1     1   2 my ( $self, $chains ) = @_;
102 1 50       4 return unless $chains;
103 1         3 foreach my $chain (@$chains) {
104 1         4 my ( $class_to_override, @methods ) = @$chain;
105 1         2 my $code = pop @methods;
106              
107 1 50       4 unless (@methods) {
108 0         0 Carp::confess("Must have at least one method name to call on $class_to_override");
109             }
110 1 50       6 unless ( 'CODE' eq ref $code ) {
111 0         0 Carp::confess("Final argument to install_method_chain must be a coderef");
112             }
113              
114 1         3 $self->_assert_looks_like_package($class_to_override);
115 1         4 $self->_assert_looks_like_subroutine($_) foreach @methods;
116              
117 1         3 my $first = shift @methods;
118 1         2 my $inflate = $code;
119 1         4 while ( my $method = pop @methods ) {
120 4         14 $inflate = { $method => $inflate };
121             }
122             {
123 2     2   10 no strict 'refs';
  2         5  
  2         976  
  1         2  
124 1         8 *{"${class_to_override}::$first"} = sub {
125 1     1   1494 Package::Butcher::Inflator->new($inflate);
126 1         4 };
127             }
128             }
129 1         3 return;
130             }
131              
132             sub _uniq {
133 1     1   4 my %seen = ();
134 1         2 grep { not $seen{$_}++ } @_;
  4         16  
135             }
136              
137             sub _predeclare {
138 1     1   2 my ( $self, $subs ) = @_;
139 1 50       4 return unless $subs;
140 1 50       5 $subs = [$subs] unless 'ARRAY' eq ref $subs;
141 1         3 my $installed = $self->subs_installed;
142              
143             # we have to predeclare subs we're installing lest we hit subtle parsing
144             # issues where Perl thinks they're indirect method calls. See 'perldoc
145             # perlobj' for more information. Thanks for Flavio for spotting this
146             # issue.
147 1         6 @$subs = _uniq(@$subs, keys %$installed);
148 1         4 my $package = $self->package;
149 1         2 my $forward_declarations = join '' => map { "sub $_;" } @$subs;
  4         12  
150 1         89 eval "package $package; $forward_declarations";
151 1 50       6 Carp::confess($@) if $@;
152             }
153              
154             sub use {
155 1     1 1 10 my ( $self, @import ) = @_;
156 1         4 $self->_load( 'use', @import );
157             }
158              
159             sub require {
160 0     0 1 0 my ($self) = @_;
161 0 0       0 if ( @_ > 1 ) {
162 0         0 Carp::confess("require() does not take arguments");
163             }
164 0         0 $self->_load('require');
165             }
166              
167             sub _load {
168 1     1   3 my ( $self, $use_or_require, @import ) = @_;
169              
170 1         3 my $package = $self->package;
171 1 50       3 if ( my $loaded = $self->is_package_loaded ) {
172 0         0 Carp::confess("You have already loaded '$package' via '$loaded'");
173             }
174              
175 1         3 my $caller = caller(1);
176              
177 1         2 my $import = '';
178 1 50       3 if (@import) {
179 1         1253 require Data::Dumper;
180 2     2   13 no warnings 'once';
  2         4  
  2         339  
181 1         7192 local $Data::Dumper::Terse = 1;
182 1         9 local $Data::Dumper::Indent = 0;
183 1         6 $import = join ', ' => Data::Dumper::Dumper(@import);
184             }
185              
186 1 50       89 my $import_list = $self->import_on_use ? $import : '()';
187 1     1   91 eval <<" USE";
  1         406  
  1         278  
  1         10  
188             package $caller;
189             $use_or_require $package $import_list;
190             USE
191 1 50       7 Carp::confess($@) if $@;
192              
193 1         4 my $to_install = $self->subs_installed;
194 1         5 foreach my $sub (keys %$to_install) {
195             # XXX we were going to do nifty checks to see if you could 'install'
196             # or 'replace' a sub, but merely the existance of
197             # Some::Package::subname() in another package would create the stash
198             # slot. We're taking the easy way out.
199             # my $stash = do { no strict 'refs'; \%{"${package}::"} };
200             # if (exists $stash->{$sub} ) {}
201 2     2   11 no strict 'refs';
  2         11  
  2         76  
202 2     2   12 no warnings 'redefine';
  2         2  
  2         271  
203 3         4 *{"${package}::$sub"} = $to_install->{$sub};
  3         17  
204             }
205              
206 1 50       4 unless ( $self->import_on_use ) {
207 1         58 eval <<" IMPORT";
208             package $caller;
209             $package->import($import);
210             IMPORT
211 1 50       5 Carp::confess($@) if $@;
212             }
213              
214 1         4 $self->_is_package_loaded($use_or_require);
215             }
216              
217             1;
218              
219             __END__