File Coverage

blib/lib/Respite/Base.pm
Criterion Covered Total %
statement 179 248 72.1
branch 88 216 40.7
condition 66 165 40.0
subroutine 28 42 66.6
pod 13 18 72.2
total 374 689 54.2


line stmt bran cond sub pod time code
1             package Respite::Base;
2              
3             # Respite::Base - base class for Respite related modules that can be used from a server or commandline
4              
5 5     5   122075 use strict;
  5         7  
  5         176  
6 5     5   21 use warnings;
  5         7  
  5         245  
7 5     5   24 use base 'Respite::Common'; # Default _configs and config
  5         7  
  5         2355  
8 5     5   2042 use autouse 'Respite::Validate' => qw(validate);
  5         4055  
  5         41  
9 5     5   359 use Scalar::Util qw(blessed weaken);
  5         11  
  5         294  
10 5     5   39 use Time::HiRes ();
  5         7  
  5         91  
11 5     5   14 use Throw qw(throw);
  5         7  
  5         26  
12              
13             our $max_recurse = 10;
14              
15       13 0   sub SHARE {}
16              
17             ###----------------------------------------------------------------###
18              
19             sub run_method {
20 0     0 0 0 my ($self, $meth, $args, $extra) = @_;
21 0   0     0 my $meta = $self->api_meta || {};
22 0 0       0 my $begin = $meta->{'log_prefix'} ? Time::HiRes::time() : undef;
23 0         0 $meth =~ tr|/.-|___|;
24 0 0       0 throw "Cannot call method", {class => ref($self), meth => $meth} if $self->_restrict($meth);
25 0   0     0 my $code = $self->find_method($meth) || throw "Invalid Respite method", {class => ref($self), method => $meth};
26 0         0 my $utf8 = $meta->{'utf8_encoded'};
27 0   0     0 my $enc = $utf8 && (!ref($utf8) || $utf8->{$meth});
28 0   0     0 my $trp = $self->{'transport'} || '';
29 0 0       0 if ($enc) { # consistently handle args from json, form, or commandline
30 0 0       0 _encode_utf8_recurse($args) if $trp eq 'json';
31             } else {
32 0 0 0     0 _decode_utf8_recurse($args) if $trp && $trp ne 'json';
33             }
34 0   0     0 my $resp = eval { $self->$code($args, $extra) } || do {
35             my $resp = $@;
36             $resp = eval { throw 'Trouble dispatching', {method => $meth, msg => $resp} } || $@ if !ref($resp) || !$resp->{'error'};
37             warn $resp if $trp ne 'cmdline';
38             $resp;
39             };
40             $self->log_request({
41             method => $meth,
42             request => $args,
43             response => $resp,
44             api_ip => $self->{'api_ip'},
45             api_brand => $self->{'api_brand'},
46             remote_ip => $self->{'remote_ip'},
47             remote_user => $self->{'remote_user'},
48             admin_user => $self->{'admin_user'},
49 0 0       0 caller => $self->{'caller'},
50             elapsed => (Time::HiRes::time() - $begin),
51             }) if $begin;
52 0 0 0     0 _decode_utf8_recurse($resp) if ref($resp) eq 'HASH' && exists($resp->{'_utf8_encoded'}) ? delete($resp->{'_utf8_encoded'}) : $enc;
    0          
53 0         0 return $resp;
54             }
55              
56             sub _restrict {
57 0     0   0 my ($class, $meth) = @_;
58 0 0       0 return 0 if __PACKAGE__->SUPER::can($meth); # any of the inherited methods from Respite::Base are not Respite methods
59 0         0 return $meth =~ /^_/;
60             }
61              
62             sub AUTOLOAD {
63 31     31   8987 my $self = shift;
64 31 50       160 my $meth = $Respite::Base::AUTOLOAD =~ /::(\w+)$/ ? $1 : throw "Invalid method", {method => $Respite::Base::AUTOLOAD};
65 31 50       68 throw "Self was not passed while looking up method", {method => $meth, trace => 1} if ! blessed $self;
66 31   50     118 local $self->{'_autoload'}->{$meth} = ($self->{'_autoload'}->{$meth} || 0) + 1;
67 31 50       78 throw "Recursive method lookup", {class => ref($self), method => $meth} if $self->{'_autoload'}->{$meth} > $max_recurse;
68 31   66     67 my $code = $self->find_method($meth) || throw "Invalid Respite method during AUTOLOAD", {class => ref($self), method => $meth}, 1;
69 30         64 return $self->$code(@_);
70             }
71              
72       0     sub DESTROY {}
73              
74             sub api_meta {
75 9     9 0 9 my $self = shift;
76 9         13 my $ref = ref $self;
77 5     5   4229 no strict 'refs'; ## no critic
  5         6  
  5         8360  
78 9 50       8 return ${"${ref}::api_meta"} if ${"${ref}::api_meta"};
  0         0  
  9         26  
79 9 0 33     30 return $self->{'api_meta'} ||= ($ref eq __PACKAGE__ ? throw "No api_meta defined", {class => $self, type => 'no_meta'} : {});
80             }
81              
82 0     0 1 0 sub api_preload { shift->find_method; return 1 }
  0         0  
83              
84             sub _encode_utf8_recurse {
85 0     0   0 my $d = shift;
86 0 0       0 if (UNIVERSAL::isa($d, 'HASH')) {
    0          
87 0 0 0     0 for my $k (keys %$d) { my $v = $d->{$k}; (ref $v) ? _encode_utf8_recurse($v) : $v and utf8::is_utf8($v) and utf8::encode($d->{$k}) }
  0 0       0  
  0         0  
88             } elsif (UNIVERSAL::isa($d, 'ARRAY')) {
89 0 0 0     0 for my $v (@$d) { (ref $v) ? _encode_utf8_recurse($v) : $v and utf8::is_utf8($v) and utf8::encode($v) }
  0 0       0  
90             }
91             }
92              
93             sub _decode_utf8_recurse {
94 0     0   0 my $d = shift;
95 0   0     0 my $seen = shift || {};
96 0 0       0 return if $seen->{$d}++;
97 0 0       0 if (UNIVERSAL::isa($d, 'HASH')) {
    0          
98 0 0 0     0 for my $k (keys %$d) { my $v = $d->{$k}; (ref $v) ? _decode_utf8_recurse($v, $seen) : $v and !utf8::is_utf8($v) and utf8::decode($d->{$k}) }
  0 0       0  
  0         0  
99             } elsif (UNIVERSAL::isa($d, 'ARRAY')) {
100 0 0 0     0 for my $v (@$d) { (ref $v) ? _decode_utf8_recurse($v, $seen) : $v and !utf8::is_utf8($v) and utf8::decode($v) }
  0 0       0  
101             }
102             }
103              
104             ###----------------------------------------------------------------###
105              
106             sub validate_args {
107 3     3 1 2528 my ($self, $args, $val_hash) = @_;
108 3   33     33 my $sub = (caller(my $n = 1))[3]; $sub = (caller ++$n)[3] while $sub eq '(eval)' || $sub =~ /::validate_args$/;
  3         22  
109 3 50       9 if (! $val_hash) {
110 3 50       24 my $code = $self->can("${sub}__meta") or throw "Could not find meta information.", {method => $sub}, 1;
111 3         8 my $meta = $code->($self);
112 3   33     23 $val_hash = $meta->{'args'} || throw "Missing args in meta information", {method => $sub}, 1;
113 3 0 0     12 if (my $ra = $meta->{'requires_admin'} and (eval { $self->api_meta->{'enforce_requires_admin'} } || do { my $e = $@; die $e if $e && (!ref($e) || $e->{'type'} ne 'no_meta'); 0 })) {
      33        
114 0 0       0 $self->require_admin(ref($ra) eq 'CODE' ? $ra->($self, $sub, $args) : ref($ra) eq 'HASH' ? $ra : {$ra => 1, method => $sub});
    0          
115             }
116             }
117 3   100     11 my $error_hash = validate($args || {}, $val_hash) || return 1;
118             throw "Failed to validate args", {
119             errors => $error_hash,
120             type => 'validation',
121 1 50       9 ($args->{'_no_trace'} ? () : (trace => 1)),
122             }, 1;
123             }
124              
125 1 0   1 1 3 sub api_ip { $_[0]->{'api_ip'} || ($_[0]->{'base'} ? $_[0]->{'base'}->api_ip : throw "Missing api_ip",0,1) }
    50          
126 1 50 33 1 1 7 sub api_brand { $_[0]->{'api_brand'} || ($_[0]->{'base'} ? $_[0]->{'base'}->api_brand : ($_[0]->is_local && $ENV{'PROV'}) || throw "Missing api_brand",0,1) }
    50          
127 0 0   0 1 0 sub remote_ip { $_[0]->{'remote_ip'} || ($_[0]->{'base'} ? $_[0]->{'base'}->remote_ip : throw "Missing remote_ip",0,1) }
    0          
128 0 0   0 1 0 sub remote_user { $_[0]->{'remote_user'} || ($_[0]->{'base'} ? $_[0]->{'base'}->remote_user : throw "Missing remote_user",0,1) }
    0          
129              
130 0 0   0 1 0 sub admin_user { $_[0]->{'admin_user'} || ($_[0]->{'base'} ? $_[0]->{'base'}->admin_user : throw "Not authenticated",0,1) }
    0          
131              
132 1 50   1 1 20 sub transport { $_[0]->{'transport'} || ($_[0]->{'base'} ? $_[0]->{'base'}->transport : '') }
    50          
133 0 0 0 0 1 0 sub is_server { exists($_[0]->{'is_server'}) ? $_[0]->{'is_server'} : ($_[0]->{'base'} && $_[0]->{'base'}->is_server) }
134              
135 0 0   0 1 0 sub is_authed { eval { shift->admin_user } ? 1 : 0 }
  0         0  
136              
137 1 50   1 1 4 sub is_local { $_[0]->transport =~ /^(?:cmdline|gui)$/ ? 1 : 0 }
138 0     0 0 0 sub who { shift->remote_user }
139              
140             sub base {
141 0     0 1 0 my $self = shift;
142 0 0       0 if (! $self->{'base'}) {
143 0 0       0 throw "Could not find base when called_from_base",0,1 if $self->{'called_from_base'};
144 0   0     0 my $class = $self->base_class || throw "Could not find a base_class when accessing base from direct source",0,1;
145 0 0       0 return $self if ref($self) eq $class;
146 0         0 (my $file = "$class.pm") =~ s|::|/|g;
147 0 0       0 eval { require $file } || throw "Could not load base_class", {msg => $@, class => $class};
  0         0  
148 0         0 $self->{'base'} = $class->new({$self->SHARE, map {$_ => $self->{$_}} qw(api_ip api_brand remote_ip remote_user admin_user is_server)});
  0         0  
149             }
150 0         0 return $self->{'base'};
151             }
152              
153 0     0 1 0 sub base_class { shift->{'base_class'} }
154              
155             ###----------------------------------------------------------------###
156              
157             sub find_method {
158 40     40 0 111 my ($self, $meth, $opt) = @_;
159 40   50     89 my $meta = $self->api_meta || {};
160              
161 40 100 100     199 my $cache = $meta->{'_cache'}->{ref($self)} ||= {%{ $meta->{'methods'} || {} }};
  13         74  
162 40 100       157 if ($meth) {
    50          
163 31 100       88 return $cache->{$meth} if exists $cache->{$meth};
164 8 50       12 return $cache->{$meth} if exists $cache->{$meth};
165 8         9 my $code;
166 8 50 33     29 return $cache->{$meth} = $code if $code = $self->can($meth) and $code ne \&{__PACKAGE__."::$meth"};
  0         0  
167 8 100       40 return $cache->{$meth} = $code if $code = $self->can("__$meth");
168             } elsif (!$cache->{'--load--'}->{'builtin'}++) {
169 5     5   42 no strict 'refs'; ## no critic
  5         7  
  5         3309  
170 9         24 my @search = ref($self);
171 9         25 while (my $pkg = shift @search) {
172 16 100       30 unshift @search, @{"${pkg}::ISA"} if $pkg ne __PACKAGE__;
  7         26  
173 16         17 for my $meth (keys %{"${pkg}::"}) {
  16         101  
174 466 100       433 next if ! defined &{"${pkg}::$meth"};
  466         867  
175 320 100       588 next if ($pkg eq __PACKAGE__) ? $meth !~ /^__/ : defined &{__PACKAGE__."::$meth"};
  32 100       105  
176 54 50       85 next if $pkg =~ /^_[a-z]/;
177 54 50 100     186 next if $meth !~ /__meta$/ && $meth !~ /^__/ && !defined &{"${pkg}::${meth}__meta"};
  9   66     29  
178 54         97 (my $name = $meth) =~ s/^__//;
179 54   66     186 $cache->{$name} ||= "${pkg}::$meth";
180             }
181             }
182             }
183              
184 14         23 foreach my $type ('namespaces', 'lib_dirs') {
185 24   100     47 my $NS = $meta->{$type} || next;
186 15 100 66     40 $NS = $cache->{'--load--'}->{'lib_dirs'} ||= $self->_load_lib_dir($NS) if $type eq 'lib_dirs';
187 15         46 foreach my $ns (sort keys %$NS) {
188 27         36 my $opt = $NS->{$ns};
189 27 100       65 $opt = {match => $opt} if ref($opt) ne 'HASH';
190 27 50       411 my $name = !$meth ? undef : ($meth !~ /^${ns}_*(\w+)$/) ? next : $opt->{'full_name'} ? $meth : $1;
    100          
    100          
191 21   33     97 my $pkg = $opt->{'pkg'} || $opt->{'package'} || do { (my $pkg = $ns) =~ s/(?:_|\b)([a-z])/\u$1/g; $pkg };
192 21 100       150 if (! $pkg->can('new')) {
193 1         4 (my $file = "$pkg.pm") =~ s|::|/|g;
194 1 50 33     3 if (! eval { require ($opt->{'file'} ||= $file) }) {
  1         887  
195 0         0 warn "Failed to load listed module $pkg ($opt->{'file'}): $@";
196 0         0 next;
197             }
198 1 50       248 $INC{$file} = $INC{$opt->{'file'}} if $opt->{'file'} ne $file;
199             }
200              
201             # TODO - faster lookup if we know the method
202 21   100     47 my $qr = $opt->{'match'} || 1;
203 21 100 66     139 $qr = ($qr eq '1' || $qr eq '*') ? qr{.} : qr{^$qr} if $qr && !ref $qr;
    50 33        
204 5     5   52 no strict 'refs'; ## no critic
  5         7  
  5         7479  
205 21         32 for my $meth (keys %{"${pkg}::"}) {
  21         109  
206 111 100       126 next if ! defined &{"${pkg}::$meth"};
  111         238  
207 53 50       83 next if ($pkg eq __PACKAGE__) ? $meth !~ /^__/ : defined &{__PACKAGE__."::$meth"};
  53 100       179  
208 49 50       103 next if $meth =~ /^_[a-z]/;
209 49 50 33     283 next if $qr && $meth !~ $qr;
210 49 100 100     149 next if $meth !~ /__meta$/ && $meth !~ /^__/ && !defined &{"${pkg}::${meth}__meta"};
  26   100     98  
211 33         65 (my $name = $meth) =~ s/^__//;
212 33 50 33     368 $name = "${ns}_${name}" if !$opt->{'full_name'} && $name !~ /^\Q$ns\E_/;
213 33   100     103 my $dt = $opt->{'dispatch_type'} || $meta->{'dispatch_type'} || 'new';
214 4     4   5 $cache->{$name} ||= ($dt eq 'new') ? sub { my $base = shift; $pkg->new({base => $base, called_from_base => 1, $base->SHARE})->$meth(@_) }
  4         10  
215             : ($dt eq 'morph') ? sub {
216 1     1   2 my $base = shift;
217 1         3 my $prev = ref $base;
218 1   33     6 local $base->{'base'} = $base->{'base'} || $base; weaken($base->{'base'});
  1         2  
219 1 50       1 my $resp; my $ok = eval { bless $base, $pkg; $resp = $base->$meth(@_); 1 }; my $err = $@; bless $base, $prev; die $err if ! $ok; return $resp;
  1         2  
  1         2  
  1         3  
  1         8  
  1         2  
  1         2  
  1         12  
  1         4  
220             }
221 13   66 13   17 : ($dt eq 'cache') ? sub { my $base = shift; ($base->{$pkg} ||= do { my $s = $pkg->new({base => $base, $base->SHARE}); weaken $s->{'base'}; $s })->$meth(@_) }
  13         40  
  7         69  
  7         35  
  7         25  
222 33 50 66     252 : throw "Unknown dispatch_type", {dispatch_type => $dt}, 1;
    100          
    100          
223             }
224 21 50 66     127 if (($meta->{'allow_nested'} || $opt->{'allow_nested'}) && defined(&{"${pkg}::api_meta"}) && $pkg->can('find_method')) {
  2   66     13  
      66        
225 2         5 my $c2 = $pkg->new({$self->SHARE})->find_method; # TODO - pass them in
226 2         10 for my $meth (keys %$c2) {
227 18 50 33     54 next if $qr && $meth !~ $qr;
228 18 50 33     54 $name = (!$opt->{'full_name'} && $meth !~ /^\Q$ns\E_/) ? "${ns}_${meth}" : $meth;
229 18         37 $cache->{$name} = $c2->{$meth};
230             }
231             }
232 21 50 66     86 return $cache->{$meth} if $meth && $cache->{$meth};
233             }
234             }
235              
236 10 100       26 return $cache->{$meth} = 0 if $meth;
237 9         38 return $cache;
238             }
239              
240             sub _load_lib_dir {
241 3     3   7 my ($self, $NS) = @_;
242 3 50       10 if ($NS eq '1') {
243 0 0       0 throw "lib_dirs cannot be 1 when accessed from Respite::Base directly" if ref($self) eq __PACKAGE__;
244 0         0 (my $file = ref($self).".pm") =~ s|::|/|g;
245 0 0 0     0 (my $dir = $INC{$file} || '') =~ s|\.pm$|| or throw "Could not determine library path location for lib_dirs", {file => $file};
246 0         0 $NS = {$dir => {pkg_prefix => ref($self)}};
247             }
248 3         3 my %h;
249 3         6 foreach my $dir (keys %$NS) {
250 3 50       214 opendir my $dh, $dir or do { warn "Failed to opendir $dir: $!"; next };
  0         0  
  0         0  
251 3         11 my $opt = $NS->{$dir};
252 3 50       14 $opt = {match => $opt} if ref($opt) ne 'HASH';
253 3 50       9 my $prefix = $opt->{'pkg_prefix'} ? "$opt->{'pkg_prefix'}::" : '';
254 3         80 foreach my $sub (readdir $dh) {
255 12 100       75 next if $sub !~ /^([a-zA-Z]\w*)\.pm$/; # TODO - possibly handle dirs
256 3         8 my $pkg = $1;
257 3 50 33     19 next if $opt->{'pkg_exclude'} && $pkg =~ $opt->{'pkg_exclude'};
258 3         34 (my $name = $pkg) =~ s/(?: (?<=[a-z])(?=[A-Z]) | (?<=[A-Z])(?=[A-Z][a-z]) )/_/xg; # FooBar => Foo_Bar, RespiteUser => Respite_User
259 3         25 $h{lc $name} = {%$opt, pkg => "$prefix$pkg", file => "$dir/$sub"};
260             }
261             }
262 3         13 return \%h;
263             }
264              
265             ###----------------------------------------------------------------###
266              
267             sub __methods__meta {
268 3   33 3   9 my $class = ref($_[0]) || $_[0];
269             return {
270 3         22 desc => "Return a list of all known $class methods. Optionally return all meta information as well",
271             args => {
272             meta => {desc => 'If true, returns all meta information for the method instead of just the description'},
273             method => {desc => 'If passed will be used to filter the available methods - can contain * as a wildcard'},
274             },
275             resp => {methods => 'hashref of available method/description pairs. Will return method/metainfo pairs if meta => 1 is passed.'},
276             };
277             }
278              
279             sub __methods {
280 2     2   4 my ($self, $args) = @_;
281 5     5   38 no strict 'refs'; ## no critic
  5         10  
  5         2679  
282 2   33     5 my $pkg = ref($self) || $self;
283 2         3 my %m;
284 2 50       5 my $qr = !$args->{'method'} ? undef : do { (my $p = $args->{'method'}) =~ s/\*/.*/g; qr/^$p$/i };
  0         0  
  0         0  
285 2         5 my $meths = $self->find_method(); # will load all
286 2         6 foreach my $meth (keys %$meths) {
287 36 100       87 next if $meth !~ /^(\w+)__meta$/;
288 14         20 my $name = $1;
289 14 50 33     24 next if $qr && $name !~ $qr;
290 14   66     16 my $meta = eval { $self->$meth() } || do { (my $err = $@ || '') =~ s/ at \/.*//s; {desc => "Not documented".($err ? ": $err" : '')} };
291 14 0 33     74 next if $ENV{'REQUEST_METHOD'} && $meta->{'no_listing'};
292 14 50       33 $m{$name} = $args->{'meta'} ? $meta : $meta->{'no_listing'} ? "(Not listed in Web Respite) $meta->{'desc'}" : $meta->{'desc'};
    50          
293 14         34 delete $meta->{'api_enum'};
294             }
295 2         9 return {methods => \%m};
296             }
297              
298             sub __hello__meta {
299             return {
300 3     3   30 desc => 'Basic call to test connection',
301             args => {test_auth => {validate_if => 'test_auth', enum => ['', 0, 1], desc => 'Optional - if passed it will require authentication'}},
302             resp => {
303             server_time => "Server epoch time",
304             args => "Echo of the passed in args",
305             api_ip => 'IP',
306             api_brand => 'Which brand is in use (if any)',
307             admin_user => 'Returned if test_auth is passed',
308             },
309             };
310             }
311              
312             sub __hello {
313 1     1   3 my ($self, $args) = @_;
314 1 50       3 sleep $args->{'sleep'} if $args->{'sleep'};
315 1 50       3 throw delete($args->{'fail'}), {args => $args} if $args->{'fail'};
316             return {
317             args => $args,
318             server_time => time(),
319             api_ip => $self->api_ip,
320             api_brand => eval { $self->api_brand } || undef,
321             ($args->{'test_auth'} && $self->require_admin ? (
322             admin_user => $self->admin_user,
323 1 50 50     5 token => $self->{'new_token'},
      33        
324             ) : ()),
325             };
326             }
327              
328             1;
329              
330             ###----------------------------------------------------------------###
331             __END__