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         636 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   4727 };
  70         116  
27              
28 70     70   37798 use Badger::Exception; # TODO: autoload
  70         160  
  70         544  
29 70     70   471 use Badger::Debug 'debug debug_up dump_data_inline dump_data dump_list dump_hash';
  70         122  
  70         232  
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 1611 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         561 } if DEBUG;
56              
57 459 100 100     2174 my $args = @_ && ref $_[0] eq HASH ? shift : { @_ };
58 459   33     1824 my $self = bless { }, ref $class || $class;
59 459         1650 $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       2413 return defined $self
64             ? $self
65             : $self->error("init() method failed\n");
66             }
67              
68             sub init {
69 60     60 1 110 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         305 $self->{ config } = shift;
73 60         117 return $self;
74             }
75              
76             sub warn {
77 8     8 1 55 my $self = shift;
78 8 50       16 return unless @_;
79              
80 8         19 my $message = join(BLANK, @_);
81 8         20 my $handlers = $self->on_warn;
82              
83 8         12 $self->debug("dispatching handlers for warn: ", $self->dump_data_inline($handlers), "\n") if DEBUG;
84 8 50 33     39 $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 287 my $self = shift;
93 71   66     197 my $class = ref $self || $self;
94 71   100     238 my $type = reftype $self || BLANK;
95 70     70   572 no strict REFS;
  70         162  
  70         2997  
96 70     70   406 no warnings ONCE;
  70         144  
  70         20096  
97              
98 71 100       164 if (@_) {
    100          
99             # don't stringify objects passed as argument
100 58 50       180 my $message = ref $_[0] ? shift : join(BLANK, map { defined($_) ? $_ : BLANK } @_);
  63 50       249  
101 58         227 my $handlers = $self->on_error;
102              
103             # set package variable
104 58         95 ${ $class.PKG.ERROR } = $message;
  58         254  
105 58         97 ${ $class.PKG.DECLINED } = 0;
  58         242  
106              
107 58 100       149 if ($type eq HASH) {
108             # set ERROR and DECLINED items in object
109 52         100 $self->{ ERROR } = $message;
110 52         94 $self->{ DECLINED } = 0;
111             }
112              
113 58 100 66     239 ($message) = $self->_dispatch_handlers( error => $handlers => $message )
114             if $handlers && @$handlers;
115              
116 58         281 $self->throw($message);
117             }
118             elsif ($type eq HASH) {
119 11         58 return $self->{ ERROR };
120             }
121             else {
122 2         3 return ${ $class.PKG.ERROR };
  2         13  
123             }
124             # not reached
125             }
126              
127             sub decline {
128 37     37 1 96 my $self = shift;
129 37   33     100 my $class = ref $self || $self;
130 37   50     141 my $type = reftype $self || BLANK;
131 37 50       110 my $reason = @_ == 1 ? shift : join(BLANK, @_);
132 70     70   513 no strict REFS;
  70         130  
  70         2692  
133 70     70   407 no warnings ONCE;
  70         156  
  70         10792  
134              
135 37         50 ${ $class.PKG.ERROR } = $reason;
  37         130  
136 37         53 ${ $class.PKG.DECLINED } = 1;
  37         99  
137              
138 37 50       89 if ($type eq HASH) {
139 37         58 $self->{ ERROR } = $reason;
140 37         74 $self->{ DECLINED } = 1;
141             }
142              
143 37         107 return undef;
144             }
145              
146             sub declined {
147 2     2 1 4 my $self = shift;
148 2   33     14 my $class = ref $self || $self;
149 2   50     10 my $type = reftype $self || BLANK;
150 70     70   535 no strict REFS;
  70         175  
  70         7795  
151             return ($type eq HASH)
152             ? $self->{ DECLINED }
153 2 50       16 : ${ $class.PKG.DECLINED };
  0         0  
154             }
155              
156             sub reason {
157 27     27 1 55 my $self = shift;
158 27   33     76 my $class = ref $self || $self;
159 27   50     114 my $type = reftype $self || BLANK;
160 70     70   535 no strict REFS;
  70         160  
  70         54474  
161             return $type eq HASH
162             ? $self->{ ERROR }
163 27 50       556 : ${ $class.PKG.ERROR };
  0         0  
164             }
165              
166             sub throw {
167 61     61 1 137 my $self = shift;
168 61         71 my $type = shift;
169 61         216 my $emod = $self->exception;
170 61         118 my $e;
171              
172             # TODO: grok file/line/sub from caller and add to exceptions
173              
174 61 100       142 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     215 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         81 $self->debug("creating new exception object chain: info => $type\n") if DEBUG;
187 58         218 $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     41 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     8 my $config = @_ && ref $_[0] eq HASH ? shift : { @_ };
207             # construct a new exception from $type and $info fields
208 2         4 $config->{ type } = $type;
209 2         13 $config->{ info } = $info;
210 2         3 $self->debug("creating new exception object: ", $self->dump_hash($config), "\n") if DEBUG;
211 2         5 $e = $emod->new($config);
212             }
213             }
214 61         199 $e->throw;
215             }
216              
217             sub try {
218 302     302 1 462 my $self = shift;
219 302 100       490 if (@_) {
220 16         23 my $method = shift;
221 16 100       33 if (wantarray) {
222 1         2 my @result = eval { $self->$method(@_) };
  1         6  
223 1 50       11 $self->decline($@) if $@;
224 1         3 return @result;
225             }
226             else {
227 15         20 my $result = eval { $self->$method(@_) };
  15         54  
228 15 100       114 $self->decline($@) if $@;
229 15         85 return $result;
230             }
231             }
232             else {
233 286         550 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 117 my $self = shift;
245 65   100     233 my $type = reftype $self || BLANK;
246 65         152 my $class = class($self);
247 65         86 my $throws;
248              
249 65 100       244 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       20 : $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     301 && $self->{ config }->{ throws };
      100        
262             }
263              
264             # fall back on looking for any package variable in class / base classes
265 65   66     263 return $throws
266             || $class->any_var(THROWS)
267             || $class->id;
268             }
269              
270             sub exception {
271 61     61 1 100 my $self = shift;
272 61   100     233 my $type = reftype $self || BLANK;
273 61         90 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       227 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     346 && $self->{ config }->{ exception };
      33        
290             }
291 61   33     237 return $emod
292             || class($self)->any_var(EXCEPTION);
293             }
294              
295             sub fatal {
296 3     3 1 6 my $self = shift;
297 3   66     17 my $class = ref $self || $self;
298 3         9 my $error = join(BLANK, @_);
299 70     70   621 no strict REFS;
  70         129  
  70         97319  
300              
301             # set package variable
302 3         14 ${ $class.PKG.ERROR } = $error;
  3         19  
303              
304 3 100 66     21 if (ref $self && reftype $self eq HASH) {
305 2         5 $self->{ ERROR } = $error;
306 2         7 $self->{ DECLINED } = 0;
307             }
308              
309 3         18 require Carp;
310 3         553 Carp::confess("Fatal badger error: ", @_);
311             }
312              
313              
314             #-----------------------------------------------------------------------
315             # messages
316             #-----------------------------------------------------------------------
317              
318             sub message {
319 1874     1874 1 2365 my $self = shift;
320 1874   33     3492 my $name = shift
321             || $self->fatal("message() called without format name");
322 1874   66     6861 my $ref = $self && reftype $self;
323 1874         2267 my $format;
324              
325             # allow $self object to have an internal messages hash
326 1874 100 66     9615 if ($self && $ref && $ref eq HASH && $self->{ messages }) {
      66        
      100        
327             $format = $self->{ messages }->{ $name }
328 1 50       18 if reftype $self->{ messages } eq HASH;
329             }
330              
331 1874 100 33     5204 $format = class($self)->hash_value( MESSAGES => $name )
332             || $self->fatal("message() called with invalid message type: $name")
333             unless defined $format;
334              
335 1874         4780 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 130 $_[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 28 $_[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   110 my $self = shift;
375 10   33     36 my $ref = ref $self || $self;
376 10         63 my ($pkg, $file, $line, $sub) = caller(0);
377 10         42 $sub = (caller(1))[3]; # subroutine the caller was called from
378 10         54 $sub =~ s/(.*):://;
379 10 100       30 my $msg = @_ ? join(BLANK, SPACE, @_) : BLANK;
380 10         45 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   120 my $self = shift;
398 69         145 my $class = class($self);
399 69         97 my $list;
400              
401 69 100 66     361 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     342 ||= $self->{ config }->{ $on_event }
      66        
406             || $class->list_vars($ON_EVENT);
407             # careful! the config value might be a single handler
408 61 100       183 $list = $self->{ $ON_EVENT } = [$list]
409             unless ref $list eq ARRAY;
410 61         86 $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       48 $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       210 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         2 push(@$list, @_);
430             }
431             else {
432 2         5 @$list = @_;
433             }
434             }
435             # push(@$list, @_);
436              
437 69         124 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   25 my ($self, $type, $handlers, @args) = @_;
450              
451 10         13 $self->debug("_dispatch handlers: ", $self->dump_data_inline($handlers), "\n") if DEBUG;
452              
453 10         19 foreach (@$handlers) {
454 12         15 my $handler = $_; # don't alias list items
455 12         13 $self->debug("dispatch handler: $handler\n") if DEBUG;
456 12 100       44 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         4 chomp($msg); # to stop Perl from adding a line
460 2         20 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     29 $handler = $self->can($handler)
467             || return $self->fatal("Invalid on_$type method: $handler");
468 3         21 @args = $handler->($self, @args);
469             }
470             }
471             elsif (ref $handler eq CODE) {
472 6         12 @args = $handler->(@args);
473             }
474             else {
475 0         0 $self->fatal("Invalid on_$type handler: $handler");
476             }
477 10         76 $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     52 last if ! @args || @args == 1 && ! $args[0];
      66        
481             }
482 8         14 $self->debug("returning ", join(', ', @args), "\n") if DEBUG;
483 8         20 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   423 my ($class, $object) = @_;
497 286   33     1719 bless \$object, ref $class || $class;
498             }
499              
500             sub AUTOLOAD {
501 572     572   840 my $self = shift;
502 572         2652 my ($method) = ($AUTOLOAD =~ /([^:]+)$/ );
503 572 100       1650 return if $method eq 'DESTROY';
504              
505             # call method on target object in eval block, and downgrade
506 286 100       428 if (wantarray) {
507 1         4 my @result = eval { $$self->$method(@_) };
  1         4  
508 1 50       17 $$self->decline($@) if $@;
509 1         6 return @result;
510             }
511             else {
512 285         310 my $result = eval { $$self->$method(@_) };
  285         712  
513 285 100       551 $$self->decline($@) if $@;
514 285         1450 return $result;
515             }
516              
517             # TODO: catch missing error methods
518             }
519              
520              
521             1;
522             __END__