File Coverage

blib/lib/Perinci/Access/Schemeless/DBI.pm
Criterion Covered Total %
statement 80 103 77.6
branch 21 46 45.6
condition 8 28 28.5
subroutine 11 13 84.6
pod 1 5 20.0
total 121 195 62.0


line stmt bran cond sub pod time code
1             package Perinci::Access::Schemeless::DBI;
2              
3             our $DATE = '2016-03-16'; # DATE
4             our $VERSION = '0.08'; # VERSION
5              
6 1     1   87514 use 5.010001;
  1         13  
7 1     1   5 use strict;
  1         2  
  1         20  
8 1     1   4 use warnings;
  1         2  
  1         33  
9 1     1   5 use experimental 'smartmatch';
  1         2  
  1         8  
10              
11 1     1   67 use JSON::MaybeXS;
  1         1  
  1         106  
12             my $json = JSON::MaybeXS->new->allow_nonref;
13              
14 1     1   13 use parent qw(Perinci::Access::Schemeless);
  1         2  
  1         10  
15              
16             sub new {
17 1     1 1 143295 my $class = shift;
18 1         23 my $self = $class->SUPER::new(@_);
19              
20             # check required attributes
21 1         8001 my $dbh = $self->{dbh};
22 1 50       6 die "Please specify required attribute 'dbh'" unless $dbh;
23              
24             # if this looks like a table created by App::UpdateRinciMetadataDb, check
25             # its version
26             {
27 1         3 my @tt = $dbh->tables(undef, undef);
  1         20  
28 1 50 33     3618 last unless grep {$_ eq 'meta' || $_ eq '"meta"' || $_ eq '"main"."meta"'} @tt;
  7 50       38  
29              
30 1         12 my ($sch_ver) = $dbh->selectrow_array(
31             "SELECT value FROM meta WHERE name='schema_version'");
32 1 50 33     165 if (!$sch_ver || $sch_ver ne '2') {
33 0         0 die "Database schema not supported, only version 2 is supported";
34             }
35             }
36              
37 1   50     27 $self->{fallback_on_completion} //= 0;
38              
39 1         5 $self;
40             }
41              
42             sub get_meta {
43 8     8 0 15493 my ($self, $req) = @_;
44              
45 8         20 my $leaf = $req->{-uri_leaf};
46              
47 8 100       22 if (length $leaf) {
48             my ($meta) = $self->{dbh}->selectrow_array(
49             "SELECT metadata FROM function WHERE package=? AND name=?", {},
50 7         78 $req->{-perl_package}, $leaf);
51 7 100       1127 if ($meta) {
52 6         78 $req->{-meta} = $json->decode($meta);
53             } else {
54 1         11 return [404, "No metadata found in database for package ".
55             "'$req->{-perl_package}' and function '$leaf'"];
56             }
57             } else {
58             # XXP check in database, if exists return if not return {v=>1.1}
59             my ($meta) = $self->{dbh}->selectrow_array(
60             "SELECT metadata FROM package WHERE name=?", {},
61 1         11 $req->{-perl_package});
62 1 50       137 if ($meta) {
63 1         13 $req->{-meta} = $json->decode($meta);
64             } else {
65 0         0 $req->{-meta} = {v=>1.1}; # empty metadata for /
66             }
67             }
68 7         26 return;
69             }
70              
71             sub action_list {
72 4     4 0 14885 my ($self, $req) = @_;
73 4         9 my $detail = $req->{detail};
74 4   50     22 my $f_type = $req->{type} || "";
75              
76 4         7 my @res;
77              
78             # XXX duplicated code with parent class
79             my $filter_path = sub {
80 13     13   24 my $path = shift;
81 13 50 33     35 if (defined($self->{allow_paths}) &&
82             !Perinci::Access::Schemeless::__match_paths2($path, $self->{allow_paths})) {
83 0         0 return 0;
84             }
85 13 50 33     33 if (defined($self->{deny_paths}) &&
86             Perinci::Access::Schemeless::__match_paths2($path, $self->{deny_paths})) {
87 0         0 return 0;
88             }
89 13         31 1;
90 4         18 };
91              
92 4         9 my $sth;
93             my %mem;
94              
95 4         9 my $pkg = $req->{-perl_package};
96              
97             # get subpackages
98 4 50 33     15 unless ($f_type && $f_type ne 'package') {
99 4 50       12 if (length $pkg) {
100             $sth = $self->{dbh}->prepare(
101 4         31 "SELECT name FROM package WHERE name LIKE ? ORDER BY name");
102 4         704 $sth->execute("$pkg\::%");
103             } else {
104             $sth = $self->{dbh}->prepare(
105 0         0 "SELECT name FROM package ORDER BY name");
106 0         0 $sth->execute;
107             }
108 4         129 while (my $r = $sth->fetchrow_hashref) {
109             # strip pkg from name
110 3         16 my $m = substr($r->{name}, length($pkg));
111              
112             # strip :: prefix
113 3         16 $m =~ s/\A:://;
114              
115             # only take the first sublevel, e.g. if user requests 'foo::bar' and
116             # db lists 'foo::bar::baz::quux', then we only want 'baz'.
117 3         17 ($m) = $m =~ /(\w+)/;
118 3         7 $m .= "/";
119              
120 3 50       14 next if $mem{$m}++;
121              
122 3 100       10 if ($detail) {
123 1         17 push @res, {uri=>$m, type=>"package"};
124             } else {
125 2         30 push @res, $m;
126             }
127             }
128             }
129              
130             # get all entities from this package. XXX currently only functions
131 4         14 my $dir = $req->{-uri_dir};
132             $sth = $self->{dbh}->prepare(
133 4         22 "SELECT name FROM function WHERE package=? ORDER BY name");
134 4         570 $sth->execute($req->{-perl_package});
135 4         90 while (my $r = $sth->fetchrow_hashref) {
136 13         30 my $e = $r->{name};
137 13         33 my $path = "$dir/$e";
138 13 50       28 next unless $filter_path->($path);
139 13 50       45 my $t = $e =~ /^[%\@\$]/ ? 'variable' : 'function';
140 13 50 33     30 next if $f_type && $f_type ne $t;
141 13 100       25 if ($detail) {
142 4         62 push @res, {
143             #v=>1.1,
144             uri=>$e, type=>$t,
145             };
146             } else {
147 9         133 push @res, $e;
148             }
149             }
150              
151 4         111 [200, "OK (list action)", \@res];
152             }
153              
154             sub action_complete_arg_val {
155 0     0 0   my ($self, $req) = @_;
156              
157 0 0         goto FALLBACK unless $self->{fallback_on_completion};
158              
159 0 0         my $arg = $req->{arg} or return err(400, "Please specify arg");
160              
161 0           $self->get_meta($req);
162 0           my $c = $req->{-meta}{args}{$arg}{completion};
163 0 0 0       goto FALLBACK unless defined($c) && ref($c) ne 'CODE';
164              
165             # get meta from parent's get_meta
166 1     1   905 no warnings 'redefine';
  1         2  
  1         195  
167 0           local *get_meta = \&Perinci::Access::Schemeless::get_meta;
168 0           delete $req->{-meta};
169              
170 0           FALLBACK:
171             $self->SUPER::action_complete_arg_val($req);
172             }
173              
174             sub action_complete_arg_elem {
175 0     0 0   my ($self, $req) = @_;
176              
177 0 0         goto FALLBACK unless $self->{fallback_on_completion};
178              
179 0 0         my $arg = $req->{arg} or return err(400, "Please specify arg");
180              
181 0           my $c = $req->{-meta}{$arg}{element_completion};
182 0 0 0       goto FALLBACK unless defined($c) && ref($c) ne 'CODE';
183              
184             # get meta from parent's get_meta
185 0           local *get_meta = \&Perinci::Access::Schemeless::get_meta;
186 0           delete $req->{-meta};
187              
188 0           FALLBACK:
189             $self->SUPER::action_complete_arg_elem($req);
190             }
191              
192             1;
193             # ABSTRACT: Subclass of Perinci::Access::Schemeless which gets lists of entities (and metadata) from DBI database
194              
195             __END__
196              
197             =pod
198              
199             =encoding UTF-8
200              
201             =head1 NAME
202              
203             Perinci::Access::Schemeless::DBI - Subclass of Perinci::Access::Schemeless which gets lists of entities (and metadata) from DBI database
204              
205             =head1 VERSION
206              
207             This document describes version 0.08 of Perinci::Access::Schemeless::DBI (from Perl distribution Perinci-Access-Schemeless-DBI), released on 2016-03-16.
208              
209             =head1 SYNOPSIS
210              
211             use DBI;
212             use Perinci::Access::Schemeless::DBI;
213              
214             my $dbh = DBI->connect(...);
215             my $pa = Perinci::Access::Schemeless::DBI->new(dbh => $dbh);
216              
217             my $res;
218              
219             # will retrieve list of code entities from database
220             $res = $pa->request(list => "/Foo/");
221              
222             # will also get metadata from database
223             $res = $pa->request(meta => "/Foo/Bar/func1");
224              
225             # the rest are the same like Perinci::Access::Schemeless
226             $res = $pa->request(actions => "/Foo/");
227              
228             =head1 DESCRIPTION
229              
230             This subclass of Perinci::Access::Schemeless gets lists of code entities
231             (currently only packages and functions) from a DBI database (instead of from
232             listing Perl packages on the filesystem). It can also retrieve L<Rinci> metadata
233             from said database (instead of from C<%SPEC> package variables).
234              
235             Currently, you must have a table containing list of packages named C<package>
236             with columns C<name> (package name), C<metadata> (Rinci metadata, encoded in
237             JSON); and a table containing list of functions named C<function> with columns
238             C<package> (package name), C<name> (function name), and C<metadata> (normalized
239             Rinci metadata, encoded in JSON). Table and column names will be configurable in
240             the future. An example of the table's contents:
241              
242             name metadata
243             ---- ---------
244             Foo::Bar (null)
245             Foo::Baz {"v":"1.1"}
246              
247             package name metadata
248             ------ ---- --------
249             Foo::Bar func1 {"v":"1.1","summary":"function 1","args":{}}
250             Foo::Bar func2 {"v":"1.1","summary":"function 2","args":{}}
251             Foo::Baz func3 {"v":"1.1","summary":"function 3","args":{"a":{"schema":["int",{},{}]}}}
252              
253             =for Pod::Coverage ^(.+)$
254              
255             =head1 HOW IT WORKS
256              
257             The subclass overrides C<get_meta()> and C<action_list()>. Thus, this modifies
258             behaviors of the following Riap actions: C<list>, C<meta>, C<child_metas>.
259              
260             =head1 new(%args) => OBJ
261              
262             Aside from its parent class, this class recognizes these attributes:
263              
264             =over
265              
266             =item * dbh => OBJ (required)
267              
268             DBI database handle.
269              
270             =item * fallback_on_completion => BOOL (default: 0)
271              
272             If set to true, then for C<complete_arg_val> and C<complete_arg_elem>, if
273             metadata has a non-coderef C<completion> or C<element_completion> in its
274             argument spec, then will fallback to parent class L<Perinci::Access::Schemeless>
275             for metadata.
276              
277             =back
278              
279             =head1 METHODS
280              
281             =head1 FAQ
282              
283             =head2 Rationale for this module?
284              
285             If you have a large number of packages and functions, you might want to avoid
286             reading Perl modules on the filesystem.
287              
288             =head2 I have completion routine for my argument, completion no longer works?
289              
290             For example, suppose your function metadata is something like this:
291              
292             {
293             v => 1.1,
294             summary => 'Delete account',
295             args => {
296             name => {
297             summary => 'Account name',
298             completion => sub {
299             my %args = @_;
300             my $word = $args{word};
301             search_accounts(prefix => $word);
302             },
303             },
304             },
305             }
306              
307             When this is stored in the database, most serialization format (JSON included)
308             doesn't save the code in C<completion>. If you use L<Data::Clean::JSON>, by
309             default the coderef will be replaced with plain string C<CODE>. This prevents
310             completion to work e.g. if you request with this Riap request:
311              
312             {action=>'complete_arg_val', uri=>..., arg=>'name'}
313              
314             One solution is to fallback to its parent class L<Perinci::Access::Schemeless>
315             (which reads metadata from Perl source files) for meta request when doing
316             completion. To do this, you can set the attribute C<fallback_on_completion>.
317              
318             =head1 HOMEPAGE
319              
320             Please visit the project's homepage at L<https://metacpan.org/release/Perinci-Access-Schemeless-DBI>.
321              
322             =head1 SOURCE
323              
324             Source repository is at L<https://github.com/sharyanto/perl-Perinci-Access-Schemeless-DBI>.
325              
326             =head1 BUGS
327              
328             Please report any bugs or feature requests on the bugtracker website L<https://rt.cpan.org/Public/Dist/Display.html?Name=Perinci-Access-Schemeless-DBI>
329              
330             When submitting a bug or request, please include a test-file or a
331             patch to an existing test-file that illustrates the bug or desired
332             feature.
333              
334             =head1 SEE ALSO
335              
336             L<Riap>, L<Rinci>
337              
338             L<App::UpdateRinciMetadataDb>
339              
340             =head1 AUTHOR
341              
342             perlancar <perlancar@cpan.org>
343              
344             =head1 COPYRIGHT AND LICENSE
345              
346             This software is copyright (c) 2016 by perlancar@cpan.org.
347              
348             This is free software; you can redistribute it and/or modify it under
349             the same terms as the Perl 5 programming language system itself.
350              
351             =cut