File Coverage

blib/lib/Perinci/Access/Lite.pm
Criterion Covered Total %
statement 92 130 70.7
branch 28 68 41.1
condition 20 41 48.7
subroutine 12 12 100.0
pod 2 2 100.0
total 154 253 60.8


line stmt bran cond sub pod time code
1             package Perinci::Access::Lite;
2              
3             our $DATE = '2016-09-25'; # DATE
4             our $VERSION = '0.14'; # VERSION
5              
6 1     1   425 use 5.010001;
  1         2  
7 1     1   3 use strict;
  1         2  
  1         13  
8 1     1   3 use warnings;
  1         1  
  1         32  
9              
10 1     1   399 use Perinci::AccessUtil qw(strip_riap_stuffs_from_res);
  1         1325  
  1         106  
11              
12             sub new {
13 1     1 1 78 my ($class, %args) = @_;
14 1   50     6 $args{riap_version} //= 1.1;
15 1         3 bless \%args, $class;
16             }
17              
18             # copy-pasted from SHARYANTO::Package::Util
19             sub __package_exists {
20 1     1   8 no strict 'refs';
  1         1  
  1         129  
21              
22 3     3   5 my $pkg = shift;
23              
24 3 50       16 return unless $pkg =~ /\A\w+(::\w+)*\z/;
25 3 50       12 if ($pkg =~ s/::(\w+)\z//) {
26 3         4 return !!${$pkg . "::"}{$1 . "::"};
  3         18  
27             } else {
28 0         0 return !!$::{$pkg . "::"};
29             }
30             }
31              
32             sub request {
33 1     1   4 no strict 'refs';
  1         1  
  1         192  
34              
35 7     7 1 14859 my ($self, $action, $url, $extra) = @_;
36              
37             #say "D:request($action => $url)";
38              
39 7   50     16 $extra //= {};
40              
41 7   50     29 my $v = $extra->{v} // 1.1;
42 7 50 33     42 if ($v ne '1.1' && $v ne '1.2') {
43 0         0 return [501, "Riap protocol not supported, must be 1.1 or 1.2"];
44             }
45              
46 7         9 my $res;
47 7 50       47 if ($url =~ m!\A(?:pl:)?/(\w+(?:/\w+)*)/(\w*)\z!) {
    0          
48 7         17 my ($mod_uripath, $func) = ($1, $2);
49 7         19 (my $pkg = $mod_uripath) =~ s!/!::!g;
50 7         40 my $mod_pm = "$mod_uripath.pm";
51              
52 7         8 my $pkg_exists;
53              
54             LOAD:
55             {
56 7 100       7 last if exists $INC{$mod_pm};
  7         17  
57 3         6 $pkg_exists = __package_exists($pkg);
58             # special names
59 3 50       9 last LOAD if $pkg =~ /\A(main)\z/;
60 3 50 33     8 last if $pkg_exists && defined(${"$pkg\::VERSION"});
  0         0  
61             #say "D:Loading $pkg ...";
62 3         3 eval { require $mod_pm };
  3         1287  
63 3 100       10618 return [500, "Can't load module $pkg: $@"] if $@;
64             }
65              
66 6 100 66     35 if ($action eq 'list') {
    50          
    50          
67 2 100       9 return [501, "Action 'list' not implemented for ".
68             "non-package entities"]
69             if length($func);
70 1     1   4 no strict 'refs';
  1         1  
  1         203  
71 1         1 my $spec = \%{"$pkg\::SPEC"};
  1         5  
72 1         4 return [200, "OK (list)", [grep {/\A\w+\z/} sort keys %$spec]];
  2         10  
73             } elsif ($action eq 'info') {
74 0 0       0 my $data = {
    0          
    0          
75             uri => "$mod_uripath/$func",
76             type => (!length($func) ? "package" :
77             $func =~ /\A\w+\z/ ? "function" :
78             $func =~ /\A[\@\$\%]/ ? "variable" :
79             "?"),
80             };
81 0         0 return [200, "OK (info)", $data];
82             } elsif ($action eq 'meta' || $action eq 'call') {
83 4 100 100     15 return [501, "Action 'call' not implemented for package entity"]
84             if !length($func) && $action eq 'call';
85 3         5 my $meta;
86             {
87 1     1   4 no strict 'refs';
  1         1  
  1         254  
  3         17  
88 3 100       7 if (length $func) {
89 2 0       1 $meta = ${"$pkg\::SPEC"}{$func}
  2 50       11  
90             or return [
91             500, "No metadata for '$url' (".
92             ($pkg_exists ? "package '$pkg' exists, perhaps you mentioned '$pkg' somewhere without actually loading the module, or perhaps '$func' is a typo?" :
93             "package '$pkg' doesn't exist, perhaps '$mod_uripath' or '$func' is a typo?") .
94             ")"];
95             } else {
96 1   50     1 $meta = ${"$pkg\::SPEC"}{':package'} // {v=>1.1};
  1         6  
97             }
98 3   66     7 $meta->{entity_v} //= ${"$pkg\::VERSION"};
  2         7  
99 3   66     8 $meta->{entity_date} //= ${"$pkg\::DATE"};
  2         5  
100             }
101              
102 3         427 require Perinci::Sub::Normalize;
103 3         859 $meta = Perinci::Sub::Normalize::normalize_function_metadata($meta);
104 3 100       3792 if ($action eq 'meta') {
105 2         4 $meta->{_orig_args_as} = $meta->{args_as};
106 2         4 $meta->{args_as} = 'hash';
107 2         2 $meta->{_orig_result_naked} = $meta->{result_naked};
108 2         3 $meta->{result_naked} = 0;
109 2         9 return [200, "OK ($action)", $meta];
110             }
111              
112             # form args (and add special args)
113 1   50     1 my $args = { %{$extra->{args} // {}} }; # shallow copy
  1         8  
114 1 0 33     3 if ($meta->{features} && $meta->{features}{progress}) {
115 0         0 require Progress::Any;
116 0         0 $args->{-progress} = Progress::Any->get_indicator;
117             }
118              
119             # convert args
120 1   50     15 my $aa = $meta->{args_as} // 'hash';
121 1         1 my @args;
122 1 50       5 if ($aa =~ /array/) {
    50          
123 0         0 require Perinci::Sub::ConvertArgs::Array;
124 0         0 my $convres = Perinci::Sub::ConvertArgs::Array::convert_args_to_array(
125             args => $args, meta => $meta,
126             );
127 0 0       0 return $convres unless $convres->[0] == 200;
128 0 0       0 if ($aa =~ /ref/) {
129 0         0 @args = ($convres->[2]);
130             } else {
131 0         0 @args = @{ $convres->[2] };
  0         0  
132             }
133             } elsif ($aa eq 'hashref') {
134 0         0 @args = ({ %$args });
135             } else {
136             # hash
137 1         3 @args = %$args;
138             }
139              
140             # call!
141             {
142 1     1   4 no strict 'refs';
  1         1  
  1         476  
  1         1  
143 1         2 $res = &{"$pkg\::$func"}(@args);
  1         6  
144             }
145              
146             # add envelope
147 1 50       56 if ($meta->{result_naked}) {
148 0         0 $res = [200, "OK (envelope added by ".__PACKAGE__.")", $res];
149             }
150              
151             # add hint that result is binary
152 1 50       4 if (defined $res->[2]) {
153 1 50 33     20 if ($meta->{result} && $meta->{result}{schema} &&
      33        
154             $meta->{result}{schema}[0] eq 'buf') {
155 0         0 $res->[3]{'x.hint.result_binary'} = 1;
156             }
157             }
158              
159             } else {
160 0         0 return [501, "Unknown/unsupported action '$action'"];
161             }
162             } elsif ($url =~ m!\Ahttps?:/(/?)!i) {
163 0         0 my $is_unix = !$1;
164 0         0 my $ht;
165 0         0 require JSON;
166 0         0 state $json = JSON->new->allow_nonref;
167 0 0       0 if ($is_unix) {
168 0         0 require HTTP::Tiny::UNIX;
169 0         0 $ht = HTTP::Tiny::UNIX->new;
170             } else {
171 0         0 require HTTP::Tiny;
172 0         0 $ht = HTTP::Tiny->new;
173             }
174             my %headers = (
175             "x-riap-v" => $self->{riap_version},
176 0         0 "x-riap-action" => $action,
177             "x-riap-fmt" => "json",
178             "content-type" => "application/json",
179             );
180 0   0     0 my $args = $extra->{args} // {};
181 0         0 for (keys %$extra) {
182 0 0       0 next if /\Aargs\z/;
183 0         0 $headers{"x-riap-$_"} = $extra->{$_};
184             }
185 0         0 my $htres = $ht->post(
186             $url, {
187             headers => \%headers,
188             content => $json->encode($args),
189             });
190             return [500, "Network error: $htres->{status} - $htres->{reason}"]
191 0 0       0 if $htres->{status} != 200;
192             return [500, "Server error: didn't return JSON (".$htres->{headers}{'content-type'}.")"]
193 0 0       0 unless $htres->{headers}{'content-type'} eq 'application/json';
194             return [500, "Server error: didn't return Riap 1.1 response (".$htres->{headers}{'x-riap-v'}.")"]
195 0 0       0 unless $htres->{headers}{'x-riap-v'} =~ /\A1\.1(\.\d+)?\z/;
196 0         0 $res = $json->decode($htres->{content});
197             } else {
198 0         0 return [501, "Unsupported scheme or bad URL '$url'"];
199             }
200              
201 1         5 strip_riap_stuffs_from_res($res);
202             }
203              
204             1;
205             # ABSTRACT: A lightweight Riap client library
206              
207             __END__
208              
209             =pod
210              
211             =encoding UTF-8
212              
213             =head1 NAME
214              
215             Perinci::Access::Lite - A lightweight Riap client library
216              
217             =head1 VERSION
218              
219             This document describes version 0.14 of Perinci::Access::Lite (from Perl distribution Perinci-Access-Lite), released on 2016-09-25.
220              
221             =head1 DESCRIPTION
222              
223             This module is a lightweight alternative to L<Perinci::Access>. It has less
224             prerequisites but does fewer things. The things it supports:
225              
226             =over
227              
228             =item * Local (in-process) access to Perl modules and functions
229              
230             Currently only C<call>, C<meta>, and C<list> actions are implemented. Variables
231             and other entities are not yet supported.
232              
233             The C<list> action only gathers keys from C<%SPEC> and do not yet list
234             subpackages.
235              
236             =item * HTTP/HTTPS
237              
238             =item * HTTP over Unix socket
239              
240             =back
241              
242             Differences with Perinci::Access:
243              
244             =over
245              
246             =item * For network access, uses HTTP::Tiny module family instead of LWP
247              
248             This results in fewer dependencies.
249              
250             =item * No wrapping, no argument checking
251              
252             For 'pl' or schemeless URL, no wrapping (L<Perinci::Sub::Wrapper>) is done, only
253             normalization (using L<Perinci::Sub::Normalize>).
254              
255             =item * No transaction or logging support
256              
257             =item * No support for some schemes
258              
259             This includes: Riap::Simple over pipe/TCP socket.
260              
261             =back
262              
263             =head1 ADDED RESULT METADATA
264              
265             This class might add the following property/attribute in result metadata:
266              
267             =head2 x.hint.result_binary => bool
268              
269             If result's schema type is C<buf>, then this class will set this attribute to
270             true, to give hints to result formatters.
271              
272             =head1 ATTRIBUTES
273              
274             =head2 riap_version => float (default: 1.1)
275              
276             =head1 METHODS
277              
278             =head2 new(%attrs) => obj
279              
280             =head2 $pa->request($action, $url, $extra) => hash
281              
282             =head1 HOMEPAGE
283              
284             Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Access-Lite>.
285              
286             =head1 SOURCE
287              
288             Source repository is at L<https://github.com/perlancar/perl-Perinci-Access-Lite>.
289              
290             =head1 BUGS
291              
292             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Access-Lite>
293              
294             When submitting a bug or request, please include a test-file or a
295             patch to an existing test-file that illustrates the bug or desired
296             feature.
297              
298             =head1 SEE ALSO
299              
300             L<Perinci::Access>
301              
302             =head1 AUTHOR
303              
304             perlancar <perlancar@cpan.org>
305              
306             =head1 COPYRIGHT AND LICENSE
307              
308             This software is copyright (c) 2016 by perlancar@cpan.org.
309              
310             This is free software; you can redistribute it and/or modify it under
311             the same terms as the Perl 5 programming language system itself.
312              
313             =cut