File Coverage

inc/Spiffy.pm
Criterion Covered Total %
statement 282 438 64.3
branch 107 204 52.4
condition 36 62 58.0
subroutine 37 60 61.6
pod 0 27 0.0
total 462 791 58.4


line stmt bran cond sub pod time code
1 8     8   98 use strict; use warnings;
  8     8   14  
  8         212  
  8         35  
  8         14  
  8         459  
2             package Spiffy;
3             our $VERSION = '0.46';
4              
5 8     8   48 use Carp;
  8         10  
  8         2793  
6             require Exporter;
7             our @EXPORT = ();
8             our @EXPORT_BASE = qw(field const stub super);
9             our @EXPORT_OK = (@EXPORT_BASE, qw(id WWW XXX YYY ZZZ));
10             our %EXPORT_TAGS = (XXX => [qw(WWW XXX YYY ZZZ)]);
11              
12             my $stack_frame = 0;
13             my $dump = 'yaml';
14             my $bases_map = {};
15              
16             sub WWW; sub XXX; sub YYY; sub ZZZ;
17              
18             # This line is here to convince "autouse" into believing we are autousable.
19             sub can {
20 125 50 33 125 0 700 ($_[1] eq 'import' and caller()->isa('autouse'))
21             ? \&Exporter::import # pacify autouse's equality test
22             : $_[0]->SUPER::can($_[1]) # normal case
23             }
24              
25             # TODO
26             #
27             # Exported functions like field and super should be hidden so as not to
28             # be confused with methods that can be inherited.
29             #
30              
31             sub new {
32 75     75 0 111 my $class = shift;
33 75   33     220 $class = ref($class) || $class;
34 75         122 my $self = bless {}, $class;
35 75         162 while (@_) {
36 0         0 my $method = shift;
37 0         0 $self->$method(shift);
38             }
39 75         147 return $self;
40             }
41              
42             my $filtered_files = {};
43             my $filter_dump = 0;
44             my $filter_save = 0;
45             our $filter_result = '';
46             sub import {
47 8     8   62 no strict 'refs';
  8         17  
  8         272  
48 8     8   38 no warnings;
  8         12  
  8         9714  
49 47     47   90 my $self_package = shift;
50              
51             # XXX Using parse_arguments here might cause confusion, because the
52             # subclass's boolean_arguments and paired_arguments can conflict, causing
53             # difficult debugging. Consider using something truly local.
54 47         62 my ($args, @export_list) = do {
55             local *boolean_arguments = sub {
56 47     47   145 qw(
57             -base -Base -mixin -selfless
58             -XXX -dumper -yaml
59             -filter_dump -filter_save
60             )
61 47         354 };
62 47     47   174 local *paired_arguments = sub { qw(-package) };
  47         76  
63 47         180 $self_package->parse_arguments(@_);
64             };
65             return spiffy_mixin_import(scalar(caller(0)), $self_package, @export_list)
66 47 50       141 if $args->{-mixin};
67              
68 47 50       101 $filter_dump = 1 if $args->{-filter_dump};
69 47 50       80 $filter_save = 1 if $args->{-filter_save};
70 47 50       83 $dump = 'yaml' if $args->{-yaml};
71 47 50       96 $dump = 'dumper' if $args->{-dumper};
72              
73 47         141 local @EXPORT_BASE = @EXPORT_BASE;
74              
75 47 50       86 if ($args->{-XXX}) {
76 0 0       0 push @EXPORT_BASE, @{$EXPORT_TAGS{XXX}}
  0         0  
77             unless grep /^XXX$/, @EXPORT_BASE;
78             }
79              
80             spiffy_filter()
81             if ($args->{-selfless} or $args->{-Base}) and
82 47 100 66     446 not $filtered_files->{(caller($stack_frame))[1]}++;
      66        
83              
84 47   33     478 my $caller_package = $args->{-package} || caller($stack_frame);
85 24         264 push @{"$caller_package\::ISA"}, $self_package
86 47 100 66     172 if $args->{-Base} or $args->{-base};
87              
88 47         79 for my $class (@{all_my_bases($self_package)}) {
  47         99  
89 69 50       328 next unless $class->isa('Spiffy');
90             my @export = grep {
91 846         817 not defined &{"$caller_package\::$_"};
  846         2301  
92 69         347 } ( @{"$class\::EXPORT"},
93             ($args->{-Base} or $args->{-base})
94 69 100 66     91 ? @{"$class\::EXPORT_BASE"} : (),
  32         89  
95             );
96             my @export_ok = grep {
97 423         401 not defined &{"$caller_package\::$_"};
  423         1133  
98 69         103 } @{"$class\::EXPORT_OK"};
  69         215  
99              
100             # Avoid calling the expensive Exporter::export
101             # if there is nothing to do (optimization)
102 69         110 my %exportable = map { ($_, 1) } @export, @export_ok;
  1145         1710  
103 69 100       223 next unless keys %exportable;
104              
105 62         80 my @export_save = @{"$class\::EXPORT"};
  62         211  
106 62         78 my @export_ok_save = @{"$class\::EXPORT_OK"};
  62         156  
107 62         87 @{"$class\::EXPORT"} = @export;
  62         261  
108 62         86 @{"$class\::EXPORT_OK"} = @export_ok;
  62         192  
109             my @list = grep {
110 62         96 (my $v = $_) =~ s/^[\!\:]//;
  44         144  
111 44 100       131 $exportable{$v} or ${"$class\::EXPORT_TAGS"}{$v};
  28         122  
112             } @export_list;
113 62         6121 Exporter::export($class, $caller_package, @list);
114 62         170 @{"$class\::EXPORT"} = @export_save;
  62         223  
115 62         87 @{"$class\::EXPORT_OK"} = @export_ok_save;
  62         8383  
116             }
117             }
118              
119             sub spiffy_filter {
120 24     24 0 4384 require Filter::Util::Call;
121 24         7081 my $done = 0;
122             Filter::Util::Call::filter_add(
123             sub {
124 48 100   48   960 return 0 if $done;
125 24         55 my ($data, $end) = ('', '');
126 24         2084 while (my $status = Filter::Util::Call::filter_read()) {
127 8352 50       14012 return $status if $status < 0;
128 8352 50       10934 if (/^__(?:END|DATA)__\r?$/) {
129 0         0 $end = $_;
130 0         0 last;
131             }
132 8352         10537 $data .= $_;
133 8352         16186 $_ = '';
134             }
135 24         160 $_ = $data;
136 24         91 my @my_subs;
137 24         1907 s[^(sub\s+\w+\s+\{)(.*\n)]
138 24         1282 [${1}my \$self = shift;$2]gm;
139 24         423 s[^(sub\s+\w+)\s*\(\s*\)(\s+\{.*\n)]
  0         0  
  0         0  
140 24         53 [${1}${2}]gm;
141 24 50       118 s[^my\s+sub\s+(\w+)(\s+\{)(.*)((?s:.*?\n))\}\n]
142 0         0 [push @my_subs, $1; "\$$1 = sub$2my \$self = shift;$3$4\};\n"]gem;
143 0         0 my $preclare = '';
144             if (@my_subs) {
145 24         486 $preclare = join ',', map "\$$_", @my_subs;
146 24 50       70 $preclare = "my($preclare);";
  0         0  
  0         0  
147 24 50       50 }
  0         0  
  0         0  
148 24         1026 $_ = "use strict;use warnings;$preclare${_};1;\n$end";
149             if ($filter_dump) { print; exit }
150 24         145 if ($filter_save) { $filter_result = $_; $_ = $filter_result; }
151             $done = 1;
152             }
153             );
154 0     0 0 0 }
155 0         0  
156             sub base {
157             push @_, -base;
158             goto &import;
159 62     62 0 82 }
160              
161             sub all_my_bases {
162 62 100       206 my $class = shift;
163              
164 23         54 return $bases_map->{$class}
165 8     8   61 if defined $bases_map->{$class};
  8         12  
  8         1951  
166 23         31  
  23         100  
167 15         50 my @bases = ($class);
  15         67  
168             no strict 'refs';
169 23         41 for my $base_class (@{"${class}::ISA"}) {
170 23         48 push @bases, @{all_my_bases($base_class)};
  45         210  
171             }
172             my $used = {};
173             $bases_map->{$class} = [grep {not $used->{$_}++} @bases];
174             }
175              
176             my %code = (
177             sub_start =>
178             "sub {\n",
179             set_default =>
180             " \$_[0]->{%s} = %s\n unless exists \$_[0]->{%s};\n",
181             init =>
182             " return \$_[0]->{%s} = do { my \$self = \$_[0]; %s }\n" .
183             " unless \$#_ > 0 or defined \$_[0]->{%s};\n",
184             weak_init =>
185             " return do {\n" .
186             " \$_[0]->{%s} = do { my \$self = \$_[0]; %s };\n" .
187             " Scalar::Util::weaken(\$_[0]->{%s}) if ref \$_[0]->{%s};\n" .
188             " \$_[0]->{%s};\n" .
189             " } unless \$#_ > 0 or defined \$_[0]->{%s};\n",
190             return_if_get =>
191             " return \$_[0]->{%s} unless \$#_ > 0;\n",
192             set =>
193             " \$_[0]->{%s} = \$_[1];\n",
194             weaken =>
195             " Scalar::Util::weaken(\$_[0]->{%s}) if ref \$_[0]->{%s};\n",
196             sub_end =>
197             " return \$_[0]->{%s};\n}\n",
198 144     144 0 288 );
199 144         166  
200 8     8   56 sub field {
  8         13  
  8         2902  
201 144     144   702 my $package = caller;
  144         253  
202 144     144   602 my ($args, @values) = do {
  144         252  
203 144         439 no warnings;
204             local *boolean_arguments = sub { (qw(-weak)) };
205 144         339 local *paired_arguments = sub { (qw(-package -init)) };
206 144 50       288 Spiffy->parse_arguments(@_);
207             };
208 144 50 66     362 my ($field, $default) = @values;
209 144 50       146 $package = $args->{-package} if defined $args->{-package};
  144         728  
210 144 50       267 die "Cannot have a default for a weakened field ($field)"
211 144 100 100     649 if defined $default && $args->{-weak};
    100 66        
212             return if defined &{"${package}::$field"};
213             require Scalar::Util if $args->{-weak};
214             my $default_string =
215             ( ref($default) eq 'ARRAY' and not @$default )
216             ? '[]'
217             : (ref($default) eq 'HASH' and not keys %$default )
218 144         242 ? '{}'
219 144 100       285 : default_as_code($default);
220 32 50       88  
221 32         182 my $code = $code{sub_start};
222 32         209 if ($args->{-init}) {
223             my $fragment = $args->{-weak} ? $code{weak_init} : $code{init};
224 144 100       448 my @count = ($fragment =~ /(%s)/g);
225             $code .= sprintf $fragment, $field, $args->{-init}, ($field) x (@count - 2);
226 144         755 }
227 144         269 $code .= sprintf $code{set_default}, $field, $default_string, $field
228             if defined $default;
229 144 50       255 $code .= sprintf $code{return_if_get}, $field;
230 144         275 $code .= sprintf $code{set}, $field;
231             $code .= sprintf $code{weaken}, $field, $field
232 144 100 66 10   16260 if $args->{-weak};
  10 100 100     41  
  7 100 66     26  
  1 100 100     3  
  18 50       58  
  48 100       274  
  2 100       6  
  3 100       98  
  9 100       43  
  5 100       30  
  3 50       8  
  1 50       8  
  21 100       77  
  20 100       80  
  2 100       95  
  2 100       12  
  10 100       49  
  1 100       2  
  4 100       52  
  10 100       32  
  1 100       7  
  5 50       28  
  37 100       88  
  37 100       81  
  48 100       156  
  2 100       9  
  2 100       8  
  1 50       7  
  1 100       7  
  1 50       5  
  1         7  
  8         30  
  8         32  
  29         200  
  11         29  
  10         19  
  3         25  
  1         3  
  1         7  
  4         18  
  4         13  
  4         49  
  20         55  
  18         72  
  1         7  
  16         40  
  17         39  
  17         32  
  3         20  
  1         2  
  1         5  
  3         13  
  2         8  
  9         23  
  9         19  
  11         26  
  82         541  
  10         20  
  12         41  
  11         47  
  3         11  
  6         30  
  13         38  
  5         11  
  2         10  
  2         4  
  2         8  
  6         21  
  7         60  
  3         23  
  1         2  
  2         14  
  3         21  
  0         0  
  0         0  
233 144 50       414 $code .= sprintf $code{sub_end}, $field;
234 8     8   65  
  8         14  
  8         2081  
235 144         164 my $sub = eval $code;
  144         753  
236 144 50       595 die $@ if $@;
237             no strict 'refs';
238             *{"${package}::$field"} = $sub;
239             return $code if defined wantarray;
240 120     120 0 6085 }
241 120         55354  
242 120         314 sub default_as_code {
243 120         6112 require Data::Dumper;
244 120         383 local $Data::Dumper::Sortkeys = 1;
245 120         266 my $code = Data::Dumper::Dumper(shift);
246             $code =~ s/^\$VAR1 = //;
247             $code =~ s/;$//;
248             return $code;
249 0     0 0 0 }
250 0         0  
251 8     8   55 sub const {
  8         14  
  8         925  
252 0     0   0 my $package = caller;
  0         0  
253 0         0 my ($args, @values) = do {
254             no warnings;
255 0         0 local *paired_arguments = sub { (qw(-package)) };
256 0 0       0 Spiffy->parse_arguments(@_);
257 8     8   51 };
  8         12  
  8         1014  
258 0 0       0 my ($field, $default) = @values;
  0         0  
259 0     0   0 $package = $args->{-package} if defined $args->{-package};
  0         0  
260 0         0 no strict 'refs';
261             return if defined &{"${package}::$field"};
262             *{"${package}::$field"} = sub { $default }
263 0     0 0 0 }
264 0         0  
265 8     8   49 sub stub {
  8         19  
  8         852  
266 0     0   0 my $package = caller;
  0         0  
267 0         0 my ($args, @values) = do {
268             no warnings;
269 0         0 local *paired_arguments = sub { (qw(-package)) };
270 0 0       0 Spiffy->parse_arguments(@_);
271 8     8   55 };
  8         21  
  8         4453  
272 0 0       0 my ($field, $default) = @values;
  0         0  
273 0         0 $package = $args->{-package} if defined $args->{-package};
274             no strict 'refs';
275 0     0   0 return if defined &{"${package}::$field"};
276 0         0 *{"${package}::$field"} =
277             sub {
278             require Carp;
279 0         0 Carp::confess
280             "Method $field in package $package must be subclassed";
281             }
282 191     191 0 311 }
283 191         346  
284 191         409 sub parse_arguments {
  567         1160  
285 191         482 my $class = shift;
  335         628  
286 191         465 my ($args, @values) = ({}, ());
287 294         368 my %booleans = map { ($_, 1) } $class->boolean_arguments;
288 294 100 66     1666 my %pairs = map { ($_, 1) } $class->paired_arguments;
    100 66        
      66        
289 24 50 33     128 while (@_) {
290             my $elem = shift;
291             if (defined $elem and defined $booleans{$elem}) {
292             $args->{$elem} = (@_ and $_[0] =~ /^[01]$/)
293             ? shift
294 32         101 : 1;
295             }
296             elsif (defined $elem and defined $pairs{$elem} and @_) {
297 238         589 $args->{$elem} = shift;
298             }
299             else {
300 191 50       1475 push @values, $elem;
301             }
302             }
303 0     0 0 0 return wantarray ? ($args, @values) : $args;
304 0     0 0 0 }
305              
306             sub boolean_arguments { () }
307             sub paired_arguments { () }
308 0 0   0 0 0  
309 0 0       0 # get a unique id for any node
310 0 0       0 sub id {
311 0         0 if (not ref $_[0]) {
312             return 'undef' if not defined $_[0];
313 0         0 \$_[0] =~ /\((\w+)\)$/o or die;
314 0 0       0 return "$1-S";
315 0         0 }
316             require overload;
317             overload::StrVal($_[0]) =~ /\((\w+)\)$/o or die;
318             return $1;
319             }
320              
321             #===============================================================================
322             # It's super, man.
323 8     8   60 #===============================================================================
  8         13  
  8         2045  
324             package DB;
325 0 0   0 0 0 {
326 0         0 no warnings 'redefine';
327             sub super_args {
328             my @dummy = caller(@_ ? $_[0] : 2);
329             return @DB::args;
330             }
331             }
332 0     0 0 0  
333 0         0 package Spiffy;
334 0         0 sub super {
335 0 0       0 my $method;
336             my $frame = 1;
337 0         0 while ($method = (caller($frame++))[3]) {
338 0 0       0 $method =~ s/.*::// and last;
339 0 0       0 }
340 0         0 my @args = DB::super_args($frame);
341 0         0 @_ = @_ ? ($args[0], @_) : @args;
342             my $class = ref $_[0] ? ref $_[0] : $_[0];
343 0 0 0     0 my $caller_class = caller;
344 0         0 my $seen = 0;
  0         0  
345 0         0 my @super_classes = reverse grep {
346 8     8   54 ($seen or $seen = ($_ eq $caller_class)) ? 0 : 1;
  8         12  
  8         1498  
347 0 0       0 } reverse @{all_my_bases($class)};
348 0 0       0 for my $super_class (@super_classes) {
  0         0  
349 0 0       0 no strict 'refs';
  0         0  
  0         0  
350             next if $super_class eq $class;
351 0         0 if (defined &{"${super_class}::$method"}) {
  0         0  
352             ${"$super_class\::AUTOLOAD"} = ${"$class\::AUTOLOAD"}
353             if $method eq 'AUTOLOAD';
354 0         0 return &{"${super_class}::$method"};
355             }
356             }
357             return;
358             }
359              
360             #===============================================================================
361             # This code deserves a spanking, because it is being very naughty.
362             # It is exchanging base.pm's import() for its own, so that people
363             # can use base.pm with Spiffy modules, without being the wiser.
364             #===============================================================================
365             my $real_base_import;
366 8 50   8   42 my $real_mixin_import;
367 8   50     63  
368 8         14 BEGIN {
369 8         22 require base unless defined $INC{'base.pm'};
370 8     8   54 $INC{'mixin.pm'} ||= 'Spiffy/mixin.pm';
  8         14  
  8         597  
371 8         75 $real_base_import = \&base::import;
372 8         511 $real_mixin_import = \&mixin::import;
373             no warnings;
374             *base::import = \&spiffy_base_import;
375             *mixin::import = \&spiffy_mixin_import;
376             }
377              
378             # my $i = 0;
379             # while (my $caller = caller($i++)) {
380             # next unless $caller eq 'base' or $caller eq 'mixin';
381             # croak <
382             # Spiffy.pm must be loaded before calling 'use base' or 'use mixin' with a
383             # Spiffy module. See the documentation of Spiffy.pm for details.
384             # END
385 59     59 0 104600 # }
386 59         102  
387 8     8   50 sub spiffy_base_import {
  8         23  
  8         2458  
388             my @base_classes = @_;
389             shift @base_classes;
390 59 100       135 no strict 'refs';
  59 50       80  
  59         427  
391 59         26129 goto &$real_base_import
392             unless grep {
393 0           eval "require $_" unless %{"$_\::"};
394 0           $_->isa('Spiffy');
395 0 0         } @base_classes;
396 0 0         my $inheritor = caller(0);
397             for my $base_class (@base_classes) {
398             next if $inheritor->isa($base_class);
399 0           croak "Can't mix Spiffy and non-Spiffy classes in 'use base'.\n",
400 0           "See the documentation of Spiffy.pm for details\n "
401 0           unless $base_class->isa('Spiffy');
402             $stack_frame = 1; # tell import to use different caller
403             import($base_class, '-base');
404             $stack_frame = 0;
405             }
406 0     0 0   }
407 0            
408 0           sub mixin {
409             my $self = shift;
410             my $target_class = ref($self);
411             spiffy_mixin_import($target_class, @_)
412 0     0 0   }
413 0 0          
414             sub spiffy_mixin_import {
415 0 0         my $target_class = shift;
416             $target_class = caller(0)
417 0           if $target_class eq 'mixin';
418 0           my $mixin_class = shift
419 0           or die "Nothing to mixin";
420 0           eval "require $mixin_class";
421 8     8   56 my @roles = @_;
  8         11  
  8         254  
422 8     8   39 my $pseudo_class = join '-', $target_class, $mixin_class, @roles;
  8         14  
  8         1134  
423 0           my %methods = spiffy_mixin_methods($mixin_class, @roles);
  0            
  0            
424 0           no strict 'refs';
  0            
425 0           no warnings;
426 0           @{"$pseudo_class\::ISA"} = @{"$target_class\::ISA"};
  0            
427             @{"$target_class\::ISA"} = ($pseudo_class);
428             for (keys %methods) {
429             *{"$pseudo_class\::$_"} = $methods{$_};
430             }
431 0     0 0   }
432 8     8   53  
  8         13  
  8         3684  
433 0           sub spiffy_mixin_methods {
434             my $mixin_class = shift;
435 0 0         no strict 'refs';
436 0           my %methods = spiffy_all_methods($mixin_class);
437 0 0         map {
  0            
438             $methods{$_}
439             ? ($_, \ &{"$methods{$_}\::$_"})
440             : ($_, \ &{"$mixin_class\::$_"})
441             } @_
442             ? (get_roles($mixin_class, @_))
443             : (keys %methods);
444 0     0 0   }
445 0            
446 0           sub get_roles {
447             my $mixin_class = shift;
448 0           my @roles = @_;
  0            
449             while (grep /^!*:/, @roles) {
450 0           @roles = map {
451 0           s/!!//g;
452             /^!:(.*)/ ? do {
453 0 0         my $m = "_role_$1";
    0          
454 0           map("!$_", $mixin_class->$m);
455 0           } :
456             /^:(.*)/ ? do {
457             my $m = "_role_$1";
458             ($mixin_class->$m);
459             } :
460 0 0 0       ($_)
461 0           } @roles;
462 0           }
463             if (@roles and $roles[0] =~ /^!/) {
464 0           my %methods = spiffy_all_methods($mixin_class);
465 0           unshift @roles, keys(%methods);
466 0           }
467 0 0         my %roles;
468             for (@roles) {
469 0           s/!!//g;
470             delete $roles{$1}, next
471 0           if /^!(.*)/;
472             $roles{$_} = 1;
473             }
474             keys %roles;
475 8     8   59 }
  8         143  
  8         1588  
476 0     0 0    
477 0 0         sub spiffy_all_methods {
478             no strict 'refs';
479 0           my $class = shift;
480             return if $class eq 'Spiffy';
481 0 0         my %methods = map {
  0            
482 0           ($_, $class)
  0            
483 0           } grep {
484 0           defined &{"$class\::$_"} and not /^_/
485 0 0         } keys %{"$class\::"};
  0            
486 0           my %super_methods;
  0            
487             %super_methods = spiffy_all_methods(${"$class\::ISA"}[0])
488             if @{"$class\::ISA"};
489             %{{%super_methods, %methods}};
490             }
491              
492              
493             # END of naughty code.
494             #===============================================================================
495 8     8   55 # Debugging support
  8         11  
  8         2838  
496 0 0   0 0   #===============================================================================
497 0           sub spiffy_dump {
498 0           no warnings;
499 0           if ($dump eq 'dumper') {
500 0           require Data::Dumper;
501             $Data::Dumper::Sortkeys = 1;
502 0           $Data::Dumper::Indent = 1;
503 0           return Data::Dumper::Dumper(@_);
504 0           }
505             require YAML;
506             $YAML::UseVersion = 0;
507             return YAML::Dump(@_) . "...\n";
508 0     0 0   }
509 0            
510             sub at_line_number {
511             my ($file_path, $line_number) = (caller(1))[1,2];
512             " at $file_path line $line_number\n";
513 0     0 0   }
514 0 0          
515             sub WWW {
516             warn spiffy_dump(@_) . at_line_number;
517             return wantarray ? @_ : $_[0];
518 0     0 0   }
519              
520             sub XXX {
521             die spiffy_dump(@_) . at_line_number;
522 0     0 0   }
523 0 0          
524             sub YYY {
525             print spiffy_dump(@_) . at_line_number;
526             return wantarray ? @_ : $_[0];
527 0     0 0   }
528 0            
529             sub ZZZ {
530             require Carp;
531             Carp::confess spiffy_dump(@_);
532             }
533              
534             1;