File Coverage

blib/lib/Class/ReluctantORM/Utilities.pm
Criterion Covered Total %
statement 178 304 58.5
branch 31 106 29.2
condition 3 19 15.7
subroutine 33 47 70.2
pod 14 17 82.3
total 259 493 52.5


line stmt bran cond sub pod time code
1             package Class::ReluctantORM::Utilities;
2 1     1   6 use warnings;
  1         23  
  1         29  
3 1     1   5 use strict;
  1         1  
  1         30  
4 1     1   6 use Data::Dumper;
  1         2  
  1         66  
5              
6             =head1 NAME
7              
8             Class::ReluctantORM::Utilities - Utility subroutines
9              
10             =head1 SYNOPSIS
11              
12             use Class::ReluctantORM::Utilities qw(:all);
13              
14             install_method('Some::Class', 'method_name', $coderef);
15             conditional_load('Class::Name');
16              
17             # Look for and load all modules under the location of Super::Class
18             # (handy for loading driver subclasses)
19             BEGIN {
20             @classes = conditional_load_subdir('Super::Class');
21             }
22              
23              
24             =head1 DESCRIPTION
25              
26             An uncohesive set of utility methods. Several are for test manipulation; some are for class loading or interface manipulation.
27              
28             No subroutines are exported by default, but all are available by request.
29              
30             =cut
31              
32 1     1   5 use base 'Exporter';
  1         2  
  1         130  
33             our @EXPORT = ();
34             our @EXPORT_OK = ();
35             our %EXPORT_TAGS = ();
36              
37 1     1   6 use Class::ReluctantORM::Exception;
  1         2  
  1         29  
38 1     1   1529 use Lingua::EN::Inflect;
  1         33915  
  1         135  
39 1     1   12 use Sub::Name;
  1         2  
  1         59  
40 1     1   8 use JSON;
  1         2  
  1         12  
41              
42             our $DEBUG = 0;
43              
44              
45             =head1 SUBROUTINES
46              
47             =cut
48              
49             =head2 install_method('Class', 'method_name', $coderef, $clobber);
50              
51             Installs a new method in a class. The class need not exist.
52              
53             $clobber determines what to do if the named method already exists. If clobber
54             is true, the existing method is renamed to __orig_method_name,
55             and $coderef is installed as method_name. If clobber is false, the existing method
56             is untouched, and the $coderef is installed as __new_method_name .
57              
58             If the named method does not exist, the coderef is
59             installed and $clobber has no effect.
60              
61             =cut
62              
63             push @EXPORT_OK, 'install_method';
64             sub install_method {
65 21     21 1 41 my ($class, $method_name, $coderef, $clobber) = @_;
66 21         229 my $existing = $class->can($method_name);
67              
68 21 50       69 unless ($coderef) { Class::ReluctantORM::Exception::Param::Missing->croak(param => 'coderef'); }
  0         0  
69              
70             {
71 1     1   254 no strict 'refs';
  1         3  
  1         40  
  21         22  
72 21 50 33     63 if ($existing && $clobber) {
    50          
73 1     1   6 no warnings 'redefine';
  1         2  
  1         539  
74 0         0 my $backup_name = '__orig_' . $method_name;
75 0         0 *{"${class}::$backup_name"} = $existing;
  0         0  
76 0         0 *{"${class}::$method_name"} = $coderef;
  0         0  
77 0         0 subname "${class}::$method_name", $coderef;
78             } elsif ($existing) {
79 0         0 my $new_name = '__new_' . $method_name;
80 0         0 *{"${class}::$new_name"} = $coderef;
  0         0  
81             } else {
82 21         19 *{"${class}::$method_name"} = $coderef;
  21         115  
83 21         197 subname "${class}::$method_name", $coderef;
84             }
85             }
86             }
87              
88             push @EXPORT_OK, 'install_method_on_first_use';
89             sub install_method_on_first_use {
90 0     0 0 0 my ($class, $method_name, $method_maker) = @_;
91 0         0 $Class::ReluctantORM::METHODS_TO_BUILD_ON_FIRST_USE{$class}{$method_name} = $method_maker;
92             }
93              
94             =head2 install_method_generator($class, $generator)
95              
96             Installs a method generator for as-needed method creation. An AUTOLOAD hook will check to see if any generator can make a method by the given name. Multuple generators can be installed for each class.
97              
98             $class is a Class::ReluctantORM subclass.
99              
100             $generator is a coderef. It will be called with two args: the class name, and the proposed method name. If the generator can generate a method body, it should do so, and return a coderef which will then be installed under that name. If no candidate can be created, it should return undef.
101              
102             =cut
103              
104             push @EXPORT_OK, 'install_method_generator';
105             sub install_method_generator {
106 0     0 1 0 my ($class, $generator) = @_;
107 0         0 push @{$Class::ReluctantORM::METHOD_GENERATORS{$class}}, $generator;
  0         0  
108             }
109              
110             our %COND_LOADED = ();
111              
112             =head2 conditional_load('Class::Name');
113              
114             Loads Class::Name if it hasn't already been loaded.
115              
116             =cut
117              
118             push @EXPORT_OK, 'conditional_load';
119             sub conditional_load {
120 20     20 0 34 my $targetClass = shift;
121 20 50       64 if (defined $COND_LOADED{$targetClass}) {
122 0 0       0 if ($COND_LOADED{$targetClass}) {
123             # Already loaded OK
124 0 0       0 if ($DEBUG) { print STDERR __PACKAGE__ . ':' . __LINE__ . "- already loaded $targetClass OK\n"; }
  0         0  
125 0         0 return;
126             }
127              
128 0         0 Class::ReluctantORM::Exception::CannotLoadClass->croak(error => 'Failed once before, not trying again.', class => $targetClass);
129             } else {
130              
131             # Check to see if it has already been loaded from file
132 20         40 my $path = $targetClass;
133 20         94 $path =~ s/::/\//g;
134 20         40 $path .= '.pm';
135 20 100       63 if (exists $INC{$path}) {
136 2 50       8 if ($DEBUG) { print STDERR __PACKAGE__ . ':' . __LINE__ . "- already loaded from file, according to \%INC, not loading $targetClass again.\n"; }
  0         0  
137 2         5 $COND_LOADED{$targetClass} = 1;
138 2         5 return;
139             }
140              
141             # Hmm, check to see if the namespace is already occupied - don't reload if it is
142             # (might have been loaded from a file not named like the class)
143 18         39 my $symbol_table_name = $targetClass . '::';
144             {
145 1     1   6 no strict 'refs';
  1         2  
  1         2800  
  18         28  
146             # Bitten by this once: check to see if the the namespace contains anything
147             # other than SYMBOLS IN ALL CAPS. The idea here is that you're likely to
148             # set things like $PACKAGE::DEBUG, which will populate the symbol table.
149             # We shouldn't consider that loaded, however; but if you have things in
150             # lowercase, you've probably got actual methods, meaning you actually already loaded.
151             # BITTEN TWICE: make sure it doesn't have :: in it either - that's a subclass that might already be loaded.
152 18 50       152 if (grep {$_ !~ /^[A-Z_0-9]+$/ } grep { $_ !~ /::/ } keys %$symbol_table_name) {
  0         0  
  0         0  
153 0 0       0 if ($DEBUG) { print STDERR __PACKAGE__ . ':' . __LINE__ . "- hmmm, apparently symbol table is non-empty, not loading $targetClass!\n"; }
  0         0  
154 0 0       0 if ($DEBUG > 2) { print STDERR __PACKAGE__ . ':' . __LINE__ . "- symbol table contents:\n" . Dumper({%$symbol_table_name}) . "\n"; }
  0         0  
155 0         0 $COND_LOADED{$targetClass} = 1;
156 0         0 return;
157             }
158             }
159              
160              
161             # OK, go ahead and load from file
162 1     1   747 eval " use $targetClass; ";
  1     1   4  
  1     1   16  
  1     1   740  
  1     1   3  
  1     1   11  
  1     1   670  
  1     1   4  
  1     1   11  
  1     1   636  
  1     1   3  
  1     1   12  
  1     1   597  
  1     1   4  
  1     1   11  
  1     1   609  
  1     1   3  
  1     1   10  
  1         615  
  1         3  
  1         10  
  1         603  
  1         4  
  1         17  
  1         742  
  1         4  
  1         28  
  1         653  
  1         3  
  1         20  
  1         617  
  1         3  
  1         18  
  1         784  
  1         6  
  1         23  
  1         766  
  1         3  
  1         20  
  1         746  
  1         3  
  1         12  
  1         837  
  1         2  
  1         13  
  1         671  
  1         3  
  1         11  
  1         597  
  1         3  
  1         12  
  1         601  
  1         3  
  1         11  
  18         1216  
163 18 50       263 if ($@) {
164 0         0 $COND_LOADED{$targetClass} = 0;
165 0         0 print STDERR __PACKAGE__ . ':' . __LINE__ . " - Exception thrown while trying to load $targetClass:\n$@\n";
166 0         0 Class::ReluctantORM::Exception::CannotLoadClass->croak(error => $@, class => $targetClass);
167             } else {
168 18 50       63 if ($DEBUG) { print STDERR __PACKAGE__ . ':' . __LINE__ . "- successfully compiled $targetClass \n"; }
  0         0  
169 18         73 $COND_LOADED{$targetClass} = 1;
170             }
171             }
172             }
173              
174             =head2 @classes = conditional_load_subdir('Super::Class', $depth);
175              
176             Finds Super::Class on the filesystem, then looks for and loads all modules Super/Class/*.pm
177             If $depth is present, directories are searched up to $depth deep (so $depth =2 gives Super/Class/*.pm Super/Class/*/*.pm Super/Class/*/*/*.pm)
178              
179             It's best to call this from within a BEGIN block, if you are trying to find driver modules.
180              
181             Returns a list of loaded classes.
182              
183             =cut
184              
185             push @EXPORT_OK, 'conditional_load_subdir';
186             sub conditional_load_subdir {
187 5     5 0 15 my $super = shift;
188 5   50     39 my $depth = shift || 0;
189              
190             # Map super class name into filename
191 5         12 my $super_fn = $super;
192 5         31 $super_fn =~ s/::/\//g;
193 5         12 my $super_stub = $super_fn;
194 5         20 $super_fn .= '.pm';
195              
196             # Lookup where it was loaded from
197 5         15 $super_fn = $INC{$super_fn};
198 5 50       22 unless ($super_fn) {
199 0         0 Class::ReluctantORM::Exception::CannotLoadClass->croak(error => 'Cannot find filesystem location, so cannot load subclasses', class => $super);
200             }
201              
202 5 50       22 if ($DEBUG) { print STDERR __PACKAGE__ . ':' . __LINE__ . "- for class $super got fs location\n$super_fn\n"; }
  0         0  
203              
204             # Build filesearch pattern
205 5         14 my $super_dir = $super_fn;
206 5         23 $super_dir =~ s/\.pm//;
207 5         8 my $glob;
208 5         18 for my $d (0..$depth) {
209 5         24 $glob .= $super_dir;
210 5         15 for (0..$d) { $glob .= '/*'; }
  5         16  
211 5         16 $glob .= '.pm ';
212             }
213              
214 5 50       16 if ($DEBUG) { print STDERR __PACKAGE__ . ':' . __LINE__ . "- got glob \n $glob\n"; }
  0         0  
215              
216             # Find files and map their names to modules
217 5         958 my @mod_files = glob($glob);
218 5         19 map { $_ =~ s/^.*$super_stub/$super_stub/; $_ } @mod_files;
  20         266  
  20         56  
219 5         11 map { $_ =~ s/\//::/g; $_ } @mod_files;
  20         74  
  20         33  
220 5         12 map { $_ =~ s/\.pm$//; $_ } @mod_files;
  20         73  
  20         32  
221              
222              
223              
224             # Load the classes
225 5         8 my @classes;
226 5         12 for my $class (@mod_files) {
227 20 50       53 if ($DEBUG) { print STDERR __PACKAGE__ . ':' . __LINE__ . "- trying to load module $class\n"; }
  0         0  
228 20         92 conditional_load($class);
229 20         59 push @classes, $class;
230             }
231 5         7398 return @classes;
232             }
233              
234             =head2 %args = check_args(%opts);
235              
236             Given the args list (which is assumed to have $self or $class already shifted off),
237             check the args list for required, optional, and mutually exclusive options.
238              
239             =over
240              
241             =item args => [] or args => {} (required)
242              
243             If a arrayref, checks to make sure it is even-numbered. If a hashref, used as-is.
244              
245             =item required => []
246              
247             List of args that are required.
248              
249             =item optional => []
250              
251             List of args that are permitted but optional.
252              
253             =item mutex => [ \@set1, \@set2, ...]
254              
255             Listref of listrefs. Each inner listref is a set of parameters that is mutually exclusive, that is, AT MOST one the params may appear.
256              
257             =item one_of => [ \@set1, \@set2, ...]
258              
259             Like mutex, but EXACTLY ONE of the params of each set must appear.
260              
261             =item frames => 2
262              
263             When throwing an exception, the number of frames to jump back. Default is 2.
264              
265             =back
266              
267             =cut
268              
269             push @EXPORT_OK, 'check_args';
270             sub check_args {
271 26 50   26 1 66 if (@_ % 2) { Class::ReluctantORM::Exception::Param::ExpectedHash->croak(); }
  0         0  
272 26         103 my %opts = @_;
273              
274 26   50     92 my $frames = $opts{frames} || 2;
275              
276 26         34 my %raw_args;
277             my %scrubbed_args;
278 26 50       66 if (ref($opts{args}) eq 'ARRAY' ) {
279 26 50       28 if (@{$opts{args}} % 2) {
  26         142  
280 0         0 Class::ReluctantORM::Exception::Param::ExpectedHash->croak(frames => $frames);
281             }
282 26         34 %raw_args = @{$opts{args}};
  26         83  
283             } else {
284 0         0 %raw_args = %{$opts{args}};
  0         0  
285             }
286              
287 26 50       63 if ($opts{debug}) { print STDERR "CA Have incoming raw_args:\n" . Dumper(\%raw_args); }
  0         0  
288              
289             # Required args
290 26 50       27 foreach my $argname (@{$opts{required} || []}) {
  26         81  
291 52 50       149 unless (exists $raw_args{$argname}) {
292 0         0 Class::ReluctantORM::Exception::Param::Missing->croak(param => $argname, frames => $frames);
293             }
294 52         90 $scrubbed_args{$argname} = $raw_args{$argname};
295 52         107 delete $raw_args{$argname};
296             }
297 26 50       67 if ($opts{debug}) { print STDERR "CA after required have raw_args:\n" . Dumper(\%raw_args); }
  0         0  
298 26 50       56 if ($opts{debug}) { print STDERR "CA after required have scrubbed_args:\n" . Dumper(\%scrubbed_args); }
  0         0  
299              
300             # Mutex and one_of
301 26         39 foreach my $mode (qw(mutex one_of)) {
302 52 50       60 foreach my $set (@{$opts{$mode} || []}) {
  52         262  
303 0         0 my $count_seen = 0;
304 0 0       0 foreach my $argname (@{$set || []}) {
  0         0  
305 0 0       0 if (exists $raw_args{$argname}) {
306 0         0 $count_seen++;
307 0         0 $scrubbed_args{$argname} = $raw_args{$argname};
308 0         0 delete $raw_args{$argname};
309             }
310             }
311 0 0 0     0 if ($mode eq 'mutex' && $count_seen > 1) {
    0 0        
    0 0        
312 0         0 Class::ReluctantORM::Exception::Param::MutuallyExclusive->croak
313             (error => 'At most one of the parameters may be supplied', param_set => $set, frames => $frames);
314             } elsif ($mode eq 'one_of' && $count_seen == 0) {
315 0         0 Class::ReluctantORM::Exception::Param::Missing->croak
316             (error => 'Exactly one of the parameters must be supplied',
317             param => join(',', @$set),
318             frames => $frames);
319             } elsif ($mode eq 'one_of' && $count_seen > 1) {
320 0         0 Class::ReluctantORM::Exception::Param::MutuallyExclusive->croak
321             (error => 'Exactly one of the parameters must be supplied',
322             param_set => $set,
323             frames => $frames);
324             }
325             }
326             }
327 26 50       69 if ($opts{debug}) { print STDERR "CA after mutex/OO have raw_args:\n" . Dumper(\%raw_args); }
  0         0  
328 26 50       50 if ($opts{debug}) { print STDERR "CA after mutex/OO have scrubbed_args:\n" . Dumper(\%scrubbed_args); }
  0         0  
329              
330             # Optional
331 26 50       31 my %still_allowed = map {$_ => 1 } @{$opts{optional} || []};
  104         274  
  26         74  
332 26         72 foreach my $argname (keys %still_allowed) {
333 104 100       217 if (exists $raw_args{$argname}) {
334 66         112 $scrubbed_args{$argname} = $raw_args{$argname};
335 66         116 delete $raw_args{$argname};
336             }
337             }
338              
339 26 50       79 if ($opts{debug}) { print STDERR "CA after optional have raw_args:\n" . Dumper(\%raw_args); }
  0         0  
340 26 50       54 if ($opts{debug}) { print STDERR "CA after optional have scrubbed_args:\n" . Dumper(\%scrubbed_args); }
  0         0  
341              
342              
343             # Spurious (raw_args should be empty now)
344 26 50       58 if (keys %raw_args) {
345 0         0 Class::ReluctantORM::Exception::Param::Spurious->croak(param => join(',', keys %raw_args), value => join(',', values %raw_args), frames => $frames);
346             }
347              
348 26         178 return %scrubbed_args;
349             }
350              
351             =head2 $usc = camel_case_to_underscore_case($camelCaseString);
352              
353             Converts a string in camel case (LikeThis) to one
354             in underscore case (like_this).
355              
356             =cut
357              
358             push @EXPORT_OK, 'camel_case_to_underscore_case';
359             sub camel_case_to_underscore_case {
360 0     0 1   my $camel = shift;
361 0           $camel =~ s/([A-Z]+)([a-z])/'_' . lc($1) . $2/ge;
  0            
362 0           $camel =~ s/^_//;
363 0           return $camel;
364             }
365              
366             =head2 my $plural = pluralize($singular);
367              
368             Returns the plural form of a word.
369              
370             =cut
371              
372             push @EXPORT_OK, 'pluralize';
373             sub pluralize {
374 0     0 1   my $singular = shift;
375              
376 0 0         if ($singular =~ /staff$/i) {
377 0           return $singular;
378             }
379             else {
380 0 0         if ($singular =~ /^[A-Z]/) {
381 0           return ucfirst( Lingua::EN::Inflect::PL( lcfirst($singular) ) );
382             }
383             else {
384 0           return Lingua::EN::Inflect::PL($singular);
385             }
386             }
387             }
388              
389             =head2 $output = nz($input, $output_if_undef);
390              
391             If $input is defined, $outout = $input.
392              
393             If $input is undef, $output = $output_if_undef.
394              
395             Named after the same function in Visual Basic, where all good ideas originate.
396              
397             =cut
398              
399             push @EXPORT_OK, 'nz';
400              
401 0 0   0 1   sub nz { return defined($_[0]) ? $_[0] : $_[1]; }
402              
403              
404             =head2 $bool = array_shallow_eq($ary1, $ary2);
405              
406             Returns true if the arrays referred to by the arrayrefs $ary1 and $ary2 are identical in a shallow sense, using 'eq'.
407              
408             =cut
409              
410             push @EXPORT_OK, 'array_shallow_eq';
411             sub array_shallow_eq {
412 0     0 1   my $ary1 = shift;
413 0           my $ary2 = shift;
414 0 0         unless (ref($ary1) eq 'ARRAY') { Class::ReluctantORM::Exception::Param::ExpectedArrayRef->croak(param => 'array1'); }
  0            
415 0 0         unless (ref($ary2) eq 'ARRAY') { Class::ReluctantORM::Exception::Param::ExpectedArrayRef->croak(param => 'array2'); }
  0            
416              
417             # Element count check
418 0 0         unless (@$ary1 == @$ary2) { return 0; }
  0            
419              
420 0           for my $i (0..(@$ary1 -1)) {
421 0           my ($c, $d) = ($ary1->[$i], $ary2->[$i]);
422 0   0       my $matched = (defined($c) && defined($d) && $c eq $d) || (!defined($c) && !defined($d));
423 0 0         return 0 unless $matched;
424             }
425 0           return 1;
426             }
427              
428             =head2 $info = last_non_cro_stack_frame();
429              
430             =head2 @frames = last_non_cro_stack_frame();
431              
432             Returns information about the the last call stack frame outside of Class::ReluctantORM.
433              
434             In scalar context, returns only the last call frame. In list context, returns the last stack frame and up.
435              
436             $info will contain keys 'file', 'package', 'line', and 'frames'. Frames indicates the value passed to caller() to obtain the information, which is the number of frames to unwind.
437              
438             =cut
439              
440             push @EXPORT_OK, 'last_non_cro_stack_frame';
441             our @PACKAGES_TO_CONSIDER_PART_OF_CRO =
442             (
443             qr{^Class::ReluctantORM},
444             );
445             sub last_non_cro_stack_frame {
446 0     0 1   my $frame = -1;
447 0           my @frames;
448              
449             FRAME:
450 0           while (1) {
451 0           $frame++;
452 0           my ($package, $file, $line) = caller($frame);
453 0 0         unless ($package) {
454             # out of frames?
455 0           return @frames;
456             }
457 0           foreach my $re (@PACKAGES_TO_CONSIDER_PART_OF_CRO) {
458 0 0         if ($package =~ $re) {
459 0           next FRAME;
460             }
461             }
462             # Didn't match anything, must not be CRO
463 0           my %info = (
464             package => $package,
465             file => $file,
466             line => $line,
467             frames => $frame,
468             );
469 0           push @frames, \%info;
470 0 0         if (!wantarray) {
471 0           return \%info;
472             }
473             }
474              
475             }
476             # TEST WINDOW - see t/29-utils.t
477             sub __testsub_lncsf1 {
478 0     0     return last_non_cro_stack_frame();
479             }
480              
481              
482             =head2 $int = row_size($hashref);
483              
484             Calculates the size, in bytes, of the values of the given hashref. This is used by the RowSize and QuerySize Monitors.
485              
486             =cut
487              
488             push @EXPORT_OK, qw(row_size);
489             sub row_size {
490 0     0 1   my $row = shift;
491 0           my $tally = 0;
492 0           foreach my $v (values %$row) {
493             # OK, actually characters, not bytes. We just want a rough size anyway. See 'perldoc -f length' for more accurate approaches
494 0 0         $tally += length($v) if defined($v); # NULL/undef count as 0, I suppose
495             }
496 0           return $tally;
497             }
498              
499             =head2 deprecated($message);
500              
501              
502             =cut
503              
504             push @EXPORT_OK, 'deprecated';
505 0     0 1   sub deprecated {
506             # TODO - write deprecated() util function
507             }
508              
509             =head2 read_file($filename)
510              
511             File::Slurp::read_file workalike, but far crappier.
512              
513             =cut
514              
515             push @EXPORT_OK, 'read_file';
516             sub read_file {
517 0     0 1   my $filename = shift;
518 0           my $out;
519             {
520 0           local( $/, *FH ) ;
  0            
521 0 0         open( FH, $filename ) or die "could not open $filename: $!\n";
522 0           $out = ;
523             }
524 0           return $out;
525             }
526              
527             =head2 write_file($filename, $content)
528              
529             File::Slurp::write_file workalike, but far crappier.
530              
531             =cut
532              
533             push @EXPORT_OK, 'write_file';
534             sub write_file {
535 0     0 1   my $filename = shift;
536 0           my $content = shift;
537             {
538 0           my $fh;
  0            
539 0 0         open( $fh, '>' . $filename ) or die "could not open $filename: $!\n";
540 0           print $fh $content;
541             }
542             }
543              
544             =head2 $json_string = json_encode($perl_ref);
545              
546             Version-blind JSON encoder.
547              
548             =cut
549              
550             push @EXPORT_OK, 'json_encode';
551             sub json_encode {
552 0 0   0 1   if ($JSON::VERSION > 2) {
553 0           goto &JSON::to_json;
554             } else {
555 0           goto &JSON::objToJson;
556             }
557             }
558              
559             =head2 $perl_ref = json_decode($json_string);
560              
561             Version-blind JSON decoder.
562              
563             =cut
564              
565             push @EXPORT_OK, 'json_decode';
566             sub json_decode {
567 0 0   0 1   if ($JSON::VERSION > 2) {
568 0           goto &JSON::from_json;
569             } else {
570 0           goto &JSON::jsonToObj;
571             }
572             }
573              
574              
575             $EXPORT_TAGS{all} = \@EXPORT_OK;
576              
577             1;
578