File Coverage

blib/lib/Net/Respite/Base.pm
Criterion Covered Total %
statement 179 252 71.0
branch 88 230 38.2
condition 66 176 37.5
subroutine 28 43 65.1
pod 13 19 68.4
total 374 720 51.9


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