File Coverage

blib/lib/Perinci/Access/Schemeless/DBI.pm
Criterion Covered Total %
statement 81 104 77.8
branch 21 46 45.6
condition 9 30 30.0
subroutine 11 13 84.6
pod 1 5 20.0
total 123 198 62.1


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