File Coverage

lib/Badger/Base.pm
Criterion Covered Total %
statement 204 217 94.0
branch 76 96 79.1
condition 69 116 59.4
subroutine 32 36 88.8
pod 20 20 100.0
total 401 485 82.6


line stmt bran cond sub pod time code
1             #========================================================================
2             #
3             # Badger::Base
4             #
5             # DESCRIPTION
6             # Base class module implementing common functionality for various
7             # other Badger modules.
8             #
9             # AUTHOR
10             # Andy Wardley
11             #
12             #========================================================================
13              
14             package Badger::Base;
15              
16             use Badger::Class
17 70         625 version => 0.01,
18             debug => 0,
19             constants => 'CODE HASH ARRAY BLANK SPACE PKG REFS ONCE WARN NONE',
20             import => 'class classes',
21             utils => 'blessed reftype xprintf',
22             words => 'ID EXCEPTION THROWS ERROR DECLINED before after',
23             constant => {
24             base_id => 'Badger', # stripped from class name to make id
25             TRIAL => 'Badger::Base::Trial',
26 70     70   4401 };
  70         114  
27              
28 70     70   37604 use Badger::Exception; # TODO: autoload
  70         196  
  70         519  
29 70     70   447 use Badger::Debug 'debug debug_up dump_data_inline dump_data dump_list dump_hash';
  70         120  
  70         209  
30              
31             our $EXCEPTION = 'Badger::Exception' unless defined $EXCEPTION;
32             our $ON_WARN = WARN;
33             our $MESSAGES = {
34             not_found => '%s not found: %s',
35             not_found_in => '%s not found in %s',
36             not_implemented => '%s is not implemented %s',
37             no_component => 'No %s component defined',
38             bad_method => "Invalid method '%s' called on %s at %s line %s",
39             invalid => 'Invalid %s specified: %s',
40             unexpected => 'Invalid %s specified: %s (expected a %s)',
41             missing_to => 'No %s specified to %s',
42             missing => 'No %s specified',
43             todo => '%s is TODO %s',
44             at_line => '%s at line %s',
45             at_file_line => '%s in %s at line %s',
46             };
47              
48              
49             sub new {
50 459     459 1 1521 my $class = shift;
51              
52             # install warning handling for odd number of parameters when DEBUG enabled
53             local $SIG{__WARN__} = sub {
54 0     0   0 Badger::Utils::odd_params(@_);
55 459         575 } if DEBUG;
56              
57 459 100 100     2135 my $args = @_ && ref $_[0] eq HASH ? shift : { @_ };
58 459   33     2139 my $self = bless { }, ref $class || $class;
59 459         1614 $self = $self->init($args);
60              
61             # be careful to account for object that overload the boolean comparison
62             # operator and may return false to a simple truth test.
63 456 50       2304 return defined $self
64             ? $self
65             : $self->error("init() method failed\n");
66             }
67              
68             sub init {
69 60     60 1 104 my $self = shift;
70             # default action is to store reference to entire configuration so
71             # that methods can examine it later if they need to
72 60         242 $self->{ config } = shift;
73 60         100 return $self;
74             }
75              
76             sub warn {
77 8     8 1 56 my $self = shift;
78 8 50       15 return unless @_;
79              
80 8         20 my $message = join(BLANK, @_);
81 8         18 my $handlers = $self->on_warn;
82              
83 8         10 $self->debug("dispatching handlers for warn: ", $self->dump_data_inline($handlers), "\n") if DEBUG;
84 8 50 33     40 $self->_dispatch_handlers( warn => $handlers => $message )
85             if $handlers && @$handlers;
86              
87             # Warning is usually raised by the last handler in the chain which
88             # defaults to 'warn', so it's OK to just drop out here.
89             }
90              
91             sub error {
92 71     71 1 246 my $self = shift;
93 71   66     174 my $class = ref $self || $self;
94 71   100     241 my $type = reftype $self || BLANK;
95 70     70   526 no strict REFS;
  70         183  
  70         3019  
96 70     70   426 no warnings ONCE;
  70         106  
  70         19504  
97              
98 71 100       166 if (@_) {
    100          
99             # don't stringify objects passed as argument
100 58 50       170 my $message = ref $_[0] ? shift : join(BLANK, map { defined($_) ? $_ : BLANK } @_);
  63 50       243  
101 58         233 my $handlers = $self->on_error;
102              
103             # set package variable
104 58         77 ${ $class.PKG.ERROR } = $message;
  58         224  
105 58         82 ${ $class.PKG.DECLINED } = 0;
  58         183  
106              
107 58 100       141 if ($type eq HASH) {
108             # set ERROR and DECLINED items in object
109 52         80 $self->{ ERROR } = $message;
110 52         85 $self->{ DECLINED } = 0;
111             }
112              
113 58 100 66     221 ($message) = $self->_dispatch_handlers( error => $handlers => $message )
114             if $handlers && @$handlers;
115              
116 58         244 $self->throw($message);
117             }
118             elsif ($type eq HASH) {
119 11         39 return $self->{ ERROR };
120             }
121             else {
122 2         3 return ${ $class.PKG.ERROR };
  2         12  
123             }
124             # not reached
125             }
126              
127             sub decline {
128 37     37 1 71 my $self = shift;
129 37   33     88 my $class = ref $self || $self;
130 37   50     129 my $type = reftype $self || BLANK;
131 37 50       94 my $reason = @_ == 1 ? shift : join(BLANK, @_);
132 70     70   529 no strict REFS;
  70         130  
  70         2545  
133 70     70   401 no warnings ONCE;
  70         162  
  70         10651  
134              
135 37         47 ${ $class.PKG.ERROR } = $reason;
  37         126  
136 37         56 ${ $class.PKG.DECLINED } = 1;
  37         87  
137              
138 37 50       93 if ($type eq HASH) {
139 37         66 $self->{ ERROR } = $reason;
140 37         70 $self->{ DECLINED } = 1;
141             }
142              
143 37         90 return undef;
144             }
145              
146             sub declined {
147 2     2 1 5 my $self = shift;
148 2   33     16 my $class = ref $self || $self;
149 2   50     9 my $type = reftype $self || BLANK;
150 70     70   532 no strict REFS;
  70         182  
  70         7635  
151             return ($type eq HASH)
152             ? $self->{ DECLINED }
153 2 50       19 : ${ $class.PKG.DECLINED };
  0         0  
154             }
155              
156             sub reason {
157 27     27 1 403 my $self = shift;
158 27   33     66 my $class = ref $self || $self;
159 27   50     82 my $type = reftype $self || BLANK;
160 70     70   456 no strict REFS;
  70         163  
  70         53062  
161             return $type eq HASH
162             ? $self->{ ERROR }
163 27 50       164 : ${ $class.PKG.ERROR };
  0         0  
164             }
165              
166             sub throw {
167 61     61 1 101 my $self = shift;
168 61         79 my $type = shift;
169 61         179 my $emod = $self->exception;
170 61         98 my $e;
171              
172             # TODO: grok file/line/sub from caller and add to exceptions
173              
174 61 100       133 if (! @_) {
175             # single argument can be an exception object or an error message
176             # which is given whatever type is returned by throws()
177             #
178             # throw($exception)
179             # throw($info)
180              
181 58 50 33     213 if (blessed $type && $type->isa($emod)) {
182 0         0 $self->debug("returning exception object: ", ref $type, " => [$type]\n") if DEBUG;
183 0         0 $e = $type;
184             }
185             else {
186 58         65 $self->debug("creating new exception object chain: info => $type\n") if DEBUG;
187 58         193 $e = $emod->new( type => $self->throws, info => $type );
188             }
189             }
190             else {
191             # Next argument can also be an exception object (e.g. when chaining
192             # exceptions) or a regular info message. In the first case, we don't
193             # re-throw the exception if it's already of the correct $type (but we
194             # do if any extra arguments are provided)
195             #
196             # throw($type, $exception)
197             # throw($type, $info)
198              
199 3         6 my $info = shift;
200              
201 3 100 66     32 if (! @_ && blessed $info && $info->isa($emod) && $info->type eq $type) {
      66        
      100        
202             # second argument is already an exception of type $type
203 1         2 $e = $info;
204             }
205             else {
206 2 50 33     17 my $config = @_ && ref $_[0] eq HASH ? shift : { @_ };
207             # construct a new exception from $type and $info fields
208 2         7 $config->{ type } = $type;
209 2         4 $config->{ info } = $info;
210 2         2 $self->debug("creating new exception object: ", $self->dump_hash($config), "\n") if DEBUG;
211 2         6 $e = $emod->new($config);
212             }
213             }
214 61         207 $e->throw;
215             }
216              
217             sub try {
218 302     302 1 440 my $self = shift;
219 302 100       494 if (@_) {
220 16         27 my $method = shift;
221 16 100       32 if (wantarray) {
222 1         7 my @result = eval { $self->$method(@_) };
  1         6  
223 1 50       6 $self->decline($@) if $@;
224 1         5 return @result;
225             }
226             else {
227 15         18 my $result = eval { $self->$method(@_) };
  15         47  
228 15 100       106 $self->decline($@) if $@;
229 15         68 return $result;
230             }
231             }
232             else {
233 286         513 return TRIAL->_bind_($self);
234             }
235             }
236              
237             sub catch {
238             # this depends on some code in Badger::Exception which I haven't
239             # written yet...
240 0     0 1 0 shift->todo;
241             }
242              
243             sub throws {
244 65     65 1 131 my $self = shift;
245 65   100     204 my $type = reftype $self || BLANK;
246 65         135 my $class = class($self);
247 65         111 my $throws;
248              
249 65 100       200 if (@_) {
    100          
250             # hash objects store exception type in $self->{ THROWS }, anything
251             # else (classes and non-hash objects) use the $THROWS package var
252             $throws = $type eq HASH
253             ? ($self->{ THROWS } = shift)
254 4 100       18 : $class->var(THROWS, shift);
255             }
256             elsif ($type eq HASH) {
257             # we also look in $self->{ config } to see if a 'throws' was
258             # specified as a constructor argument.
259             $throws = $self->{ THROWS }
260             ||= $self->{ config }
261 51   66     267 && $self->{ config }->{ throws };
      100        
262             }
263              
264             # fall back on looking for any package variable in class / base classes
265 65   66     264 return $throws
266             || $class->any_var(THROWS)
267             || $class->id;
268             }
269              
270             sub exception {
271 61     61 1 85 my $self = shift;
272 61   100     207 my $type = reftype $self || BLANK;
273 61         84 my $emod;
274              
275             # TODO: Move this into Template::Class. It's so rare that you want to
276             # set an exception type this way. Then we can have throw() pass the $type
277             # to exception() and allow subclasses to make a decision about what kind
278             # of exception to return based on the $type.
279 61 50       244 if (@_) {
    100          
280             # as per throws() above, we have to be careful to only treat $self
281             # like a hash when it is a hash-based object
282             $emod = $type eq HASH
283             ? ($self->{ EXCEPTION } = shift)
284 0 0       0 : class($self)->var(EXCEPTION, shift);
285             }
286             elsif ($type eq HASH) {
287             $emod = $self->{ EXCEPTION }
288             ||= $self->{ config }
289 55   33     309 && $self->{ config }->{ exception };
      33        
290             }
291 61   33     205 return $emod
292             || class($self)->any_var(EXCEPTION);
293             }
294              
295             sub fatal {
296 3     3 1 5 my $self = shift;
297 3   66     30 my $class = ref $self || $self;
298 3         11 my $error = join(BLANK, @_);
299 70     70   579 no strict REFS;
  70         127  
  70         94290  
300              
301             # set package variable
302 3         6 ${ $class.PKG.ERROR } = $error;
  3         14  
303              
304 3 100 66     21 if (ref $self && reftype $self eq HASH) {
305 2         6 $self->{ ERROR } = $error;
306 2         5 $self->{ DECLINED } = 0;
307             }
308              
309 3         19 require Carp;
310 3         556 Carp::confess("Fatal badger error: ", @_);
311             }
312              
313              
314             #-----------------------------------------------------------------------
315             # messages
316             #-----------------------------------------------------------------------
317              
318             sub message {
319 1874     1874 1 2233 my $self = shift;
320 1874   33     3234 my $name = shift
321             || $self->fatal("message() called without format name");
322 1874   66     6774 my $ref = $self && reftype $self;
323 1874         2094 my $format;
324              
325             # allow $self object to have an internal messages hash
326 1874 100 66     9431 if ($self && $ref && $ref eq HASH && $self->{ messages }) {
      66        
      100        
327             $format = $self->{ messages }->{ $name }
328 1 50       7 if reftype $self->{ messages } eq HASH;
329             }
330              
331 1874 100 33     5042 $format = class($self)->hash_value( MESSAGES => $name )
332             || $self->fatal("message() called with invalid message type: $name")
333             unless defined $format;
334              
335 1874         4629 xprintf($format, @_);
336             }
337              
338             sub warn_msg {
339             # explicitly quantify local message() method in case a subclass decides
340             # to re-implement the message() method to do something else
341 1     1 1 9 $_[0]->warn( message(@_) );
342             }
343              
344             sub error_msg {
345 26     26 1 107 $_[0]->error( message(@_) );
346             }
347              
348             sub fatal_msg {
349 0     0 1 0 $_[0]->fatal( message(@_) );
350             }
351              
352             sub decline_msg {
353 11     11 1 27 $_[0]->decline( message(@_) );
354             }
355              
356             sub debug_msg {
357 0     0 1 0 $_[0]->debug( message(@_) );
358             }
359              
360             sub throw_msg {
361 1     1 1 8 my $self = shift;
362 1         4 $self->throw( shift, message($self, @_) );
363             }
364              
365              
366             #-----------------------------------------------------------------------
367             # generate not_implemented() and todo() methods
368             #-----------------------------------------------------------------------
369              
370             class->methods(
371             map {
372             my $name = $_;
373             $name => sub {
374 10     10   88 my $self = shift;
375 10   33     21 my $ref = ref $self || $self;
376 10         59 my ($pkg, $file, $line, $sub) = caller(0);
377 10         39 $sub = (caller(1))[3]; # subroutine the caller was called from
378 10         50 $sub =~ s/(.*):://;
379 10 100       26 my $msg = @_ ? join(BLANK, SPACE, @_) : BLANK;
380 10         51 return $self->error_msg( $name => "$sub()$msg", "for $ref in $file at line $line" );
381             };
382             }
383             qw( not_implemented todo )
384             );
385              
386              
387             #-----------------------------------------------------------------------
388             # generate on_warn() and on_error() methods
389             #-----------------------------------------------------------------------
390              
391             class->methods(
392             map {
393             my $on_event = $_;
394             my $ON_EVENT = uc $on_event;
395              
396             $on_event => sub {
397 69     69   135 my $self = shift;
398 69         147 my $class = class($self);
399 69         93 my $list;
400              
401 69 100 66     332 if (ref $self && reftype $self eq HASH) {
402             # look in $self->{ config }->{ on_xxx } or in $ON_XXX pkg
403             # var for one or more event handlers
404             $list = $self->{ $ON_EVENT }
405 61   66     320 ||= $self->{ config }->{ $on_event }
      66        
406             || $class->list_vars($ON_EVENT);
407             # careful! the config value might be a single handler
408 61 100       146 $list = $self->{ $ON_EVENT } = [$list]
409             unless ref $list eq ARRAY;
410 61         77 $self->debug("got $on_event handlers: ", $self->dump_data_inline($list), "\n") if DEBUG;
411             }
412             else {
413             # class method or non-hash objects use pkg vars only
414 8         31 $list = $class->var_default($ON_EVENT, []);
415 8 100       40 $list = $class->var($ON_EVENT, [$list])
416             unless ref $list eq ARRAY;
417             }
418              
419             # Add to the list any extra handlers passed as args. First
420             # argument can be 'before' or 'after' to add remaining args
421             # to start or end of list, otherwise the entire list is replaced.
422 69 100       183 if (@_) {
423 3 50       10 if ($_[0] eq before) {
    100          
424 0         0 shift;
425 0         0 unshift(@$list, @_);
426             }
427             elsif ($_[0] eq after) {
428 1         2 shift;
429 1         15 push(@$list, @_);
430             }
431             else {
432 2         5 @$list = @_;
433             }
434             }
435             # push(@$list, @_);
436              
437 69         116 return $list;
438             }
439             }
440             qw( on_warn on_error )
441             );
442              
443              
444             #-----------------------------------------------------------------------
445             # internal method to dispatch on_error/on_warning handlers
446             #-----------------------------------------------------------------------
447              
448             sub _dispatch_handlers {
449 10     10   24 my ($self, $type, $handlers, @args) = @_;
450              
451 10         10 $self->debug("_dispatch handlers: ", $self->dump_data_inline($handlers), "\n") if DEBUG;
452              
453 10         16 foreach (@$handlers) {
454 12         13 my $handler = $_; # don't alias list items
455 12         13 $self->debug("dispatch handler: $handler\n") if DEBUG;
456 12 100       25 if (! ref $handler) {
    50          
457 6 100       15 if ($handler eq WARN) { # 'warn' - we make sure that the
    50          
458 2         6 my $msg = join('', @args); # message is newline terminated
459 2         3 chomp($msg); # to stop Perl from adding a line
460 2         19 CORE::warn $msg, "\n"; # number that'll be wrong.
461             }
462             elsif ($handler eq NONE) { # NONE/0 - bail out
463 0         0 last;
464             }
465             else {
466 4   100     31 $handler = $self->can($handler)
467             || return $self->fatal("Invalid on_$type method: $handler");
468 3         8 @args = $handler->($self, @args);
469             }
470             }
471             elsif (ref $handler eq CODE) {
472 6         13 @args = $handler->(@args);
473             }
474             else {
475 0         0 $self->fatal("Invalid on_$type handler: $handler");
476             }
477 10         61 $self->debug("mid-dispatch args: [", join(', ', @args), "]\n") if DEBUG;
478             # bail out if we got an empty list of return values or a single
479             # false value
480 10 100 66     49 last if ! @args || @args == 1 && ! $args[0];
      66        
481             }
482 8         9 $self->debug("returning ", join(', ', @args), "\n") if DEBUG;
483 8         17 return @args;
484             }
485              
486              
487              
488             #-----------------------------------------------------------------------
489             # Badger::Base::Trial - nomadic object for $object->try operation
490             #-----------------------------------------------------------------------
491              
492             package Badger::Base::Trial;
493             our $AUTOLOAD;
494              
495             sub _bind_ {
496 286     286   418 my ($class, $object) = @_;
497 286   33     1636 bless \$object, ref $class || $class;
498             }
499              
500             sub AUTOLOAD {
501 572     572   774 my $self = shift;
502 572         2644 my ($method) = ($AUTOLOAD =~ /([^:]+)$/ );
503 572 100       1569 return if $method eq 'DESTROY';
504              
505             # call method on target object in eval block, and downgrade
506 286 100       420 if (wantarray) {
507 1         6 my @result = eval { $$self->$method(@_) };
  1         3  
508 1 50       7 $$self->decline($@) if $@;
509 1         3 return @result;
510             }
511             else {
512 285         311 my $result = eval { $$self->$method(@_) };
  285         723  
513 285 100       547 $$self->decline($@) if $@;
514 285         1514 return $result;
515             }
516              
517             # TODO: catch missing error methods
518             }
519              
520              
521             1;
522             __END__