File Coverage

blib/lib/WebAPI/DBIC/Resource/Role/DBICParams.pm
Criterion Covered Total %
statement 15 135 11.1
branch 0 80 0.0
condition 0 16 0.0
subroutine 5 21 23.8
pod 0 3 0.0
total 20 255 7.8


line stmt bran cond sub pod time code
1             package WebAPI::DBIC::Resource::Role::DBICParams;
2             $WebAPI::DBIC::Resource::Role::DBICParams::VERSION = '0.004001';
3              
4 2     2   25233275 use Moo::Role;
  2         42752  
  2         21  
5              
6 2     2   666 use Carp;
  2         9  
  2         155  
7 2     2   13 use Scalar::Util qw(blessed);
  2         44  
  2         208  
8 2     2   1093 use Try::Tiny;
  2         4177  
  2         108  
9 2     2   1002 use Devel::Dwarn;
  2         18537  
  2         32  
10              
11             requires 'set';
12             requires 'throwable';
13             requires 'prefetch';
14              
15             # TODO the params supported by a resource should be determined by the roles
16             # consumed by that resource, plus any extra params it wants to declare support for.
17             # So this should be reworked to enable that.
18              
19              
20             # we use malformed_request() call from Web::Machine to trigger parameter processing
21             sub malformed_request {
22 0     0 0   my $self = shift;
23              
24 0           $self->handle_request_params;
25              
26 0           return 0;
27             }
28              
29              
30             # used to a) define order that params are handled,
31             # and b) to force calling of a handler even if param is missing
32             sub get_param_order {
33 0     0 0   return qw(page rows sort);
34             }
35              
36              
37             # call _handle_${basename}_param methods for each parameter
38             # where basename is the name with any .suffix removed ('me.id' => 'me')
39             sub handle_request_params {
40 0     0 0   my $self = shift;
41              
42 0           my %queue;
43 0           for my $param ($self->param) {
44 0 0         next if $param eq ""; # ignore empty parameters
45              
46 0           my @v = $self->param($param);
47             # XXX we don't handle multiple params which appear more than once
48 0 0         die "Multiple $param parameters are not supported\n" if @v > 1;
49              
50             # parameters with names containing a '.' are assumed to be search criteria
51             # this covers both 'me.field=foo' and 'relname.field=bar'
52 0 0         if ($param =~ /^\w+\.\w+/) {
53 0           $param =~ s/^me\.(\w+\.\w+)/$1/; # handle deprecated 'me.relname.fieldname' form
54 0           $queue{search_criteria}->{$param} = $v[0];
55 0           next;
56             }
57 0 0         die "Explicit search_criteria param not allowed"
58             if $param eq 'search_criteria';
59              
60             # for parameters with names like foo[x]=3&foo[y]=4
61             # we accumulate the value as a hash { x => 3, y => 4 }
62 0 0         if ($param =~ /^(\w+)\[(\w+)\]$/) {
63 0 0 0       die "$param=$v[0] can't follow $param=$queue{$param} parameter\n"
64             if $queue{$1} and not ref $queue{$1};
65 0           $queue{$1}{$2} = $v[0];
66             }
67             else {
68 0 0 0       die "$param=$v[0] can't follow $param=$queue{$param} parameter\n"
69             if $queue{$param} and ref $queue{$param};
70 0 0         $param = 'sort' if $param eq 'order'; # XXX back-compat
71 0           $queue{$param} = $v[0];
72             }
73             }
74              
75             # call handlers in desired order, then any remaining ones
76 0           my %done;
77 0           for my $param ($self->get_param_order, keys %queue) {
78 0 0         next if $done{$param}++;
79 0           my $value = delete $queue{$param};
80              
81 0           my $method = "_handle_${param}_param";
82 0 0         unless ($self->can($method)) {
83 0           die "The $param parameter is not supported by the $self resource\n";
84             }
85 0           $self->$method($value, $param);
86             }
87              
88 0           return 0;
89             }
90              
91              
92             ## no critic (ProhibitUnusedPrivateSubroutines)
93              
94             sub _handle_rows_param {
95 0     0     my ($self, $value) = @_;
96 0 0         $value = 30 unless defined $value;
97 0           $self->set( $self->set->search_rs(undef, { rows => $value }) );
98 0           return;
99             }
100              
101              
102             sub _handle_page_param {
103 0     0     my ($self, $value) = @_;
104 0 0         $value = 1 unless defined $value;
105 0           $self->set( $self->set->search_rs(undef, { page => $value }) );
106 0           return;
107             }
108              
109              
110 0     0     sub _handle_with_param { }
111              
112              
113 0     0     sub _handle_rollback_param { }
114              
115              
116             sub _handle_search_criteria_param {
117 0     0     my ($self, $value) = @_;
118 0           $self->set( $self->set->search_rs($value) );
119 0           return;
120             }
121              
122             sub _handle_prefetch_param {
123 0     0     my ($self, $value) = @_;
124              
125             # Prefetchs/join in DBIC accepts either:
126             # prefetch => relname OR
127             # prefetch => [relname1, relname2] OR
128             # prefetch => {relname1 => relname_on_relname1} OR
129             # prefetch => [{relname1 => [{relname_on_relname1 => relname_on_relname_on_relname1}, other_relname_on_relaname1]},relname2] ETC
130              
131             # Noramalise all prefetches to most complicated form.
132             # eg &prefetch=foo,bar or &prefetch.json={...}
133 0           my $prefetch = $self->_resolve_prefetch($value, $self->set->result_source);
134              
135 0 0         return unless scalar @$prefetch;
136             # XXX hack?: perhaps use {embedded}{$key} = sub { ... };
137             # see lib/WebAPI/DBIC/Resource/Role/DBIC.pm
138 0           $self->prefetch( $prefetch ); # include self, even if deleted below
139 0           $prefetch = [grep { !defined $_->{self}} @$prefetch];
  0            
140              
141 0 0         my $prefetch_or_join = $self->param('fields') ? 'join' : 'prefetch';
142 0 0         Dwarn { $prefetch_or_join => $prefetch } if $ENV{WEBAPI_DBIC_DEBUG};
143 0 0         $self->set( $self->set->search_rs(undef, { $prefetch_or_join => $prefetch }))
144             if scalar @$prefetch;
145              
146 0           return;
147             }
148              
149             sub _resolve_prefetch {
150 0     0     my ($self, $prefetch, $result_class) = @_;
151 0           my @errors;
152              
153             # Here we recursively resolve each of the prefetches to normalise them all to the most complicated
154             # form that can exist. The results will be a ArrayRef of HashRefs that can be passed to DBIC
155             # directly.
156             # This code is largely taken from the _resolve_join subroutine in DBIx::Class
157              
158 0 0 0       return [] unless defined $prefetch and length $prefetch;
159 0           my @return;
160              
161 0 0         if (ref $prefetch eq 'ARRAY') {
    0          
    0          
162 0           push @return, map {
163 0           @{$self->_resolve_prefetch($_, $result_class)}
  0            
164             } @$prefetch;
165             } elsif (ref $prefetch eq 'HASH') {
166 0           for my $rel (keys %$prefetch) {
167 0 0         next if $rel eq 'self';
168              
169 0 0         if (my @validate_errors = $self->_validate_relationship($result_class, $rel)) {
170 0           push @errors, @validate_errors;
171             } else {
172 0           push @return, {
173             $rel => $self->_resolve_prefetch($prefetch->{$rel}, $result_class->related_source($rel))
174             };
175             }
176             }
177             } elsif (ref $prefetch) {
178 0           push @errors,
179             "No idea how to resolve prefetch reftype ".ref $prefetch;
180             } else {
181 0           for my $rel (split ',', $prefetch) {
182 0           my @validate_errors = $self->_validate_relationship($result_class, $rel);
183 0 0 0       if ($rel ne 'self' && scalar @validate_errors) {
184 0           push @errors, @validate_errors;
185             } else {
186 0           push @return, {
187             $rel => [{}],
188             };
189             }
190             }
191             }
192              
193 0 0         $self->throwable->throw_bad_request(400, errors => \@errors)
194             if @errors;
195              
196 0           return \@return;
197             }
198              
199             sub _validate_relationship {
200 0     0     my ($self, $result_class, $rel) = @_;
201 0           my @errors;
202              
203             my $rel_info;
204             try {
205 0     0     $rel_info = $result_class->relationship_info($rel);
206 0           local $SIG{__DIE__}; # avoid strack trace from these dies:
207 0 0         die "no relationship with that name\n"
208             if not $rel_info;
209 0 0         die "relationship is $rel_info->{attrs}{accessor} but only single, filter and multi are supported\n"
210             if not $rel_info->{attrs}{accessor} =~ m/^(?:single|filter|multi)$/; # sanity
211             }
212             catch {
213 0     0     push @errors, {
214             $rel => $_,
215             _meta => {
216             relationship => $rel_info,
217             relationships => [ sort $result_class->relationships ]
218             }, # XXX
219             };
220 0           };
221              
222 0           return @errors;
223             }
224              
225             sub _handle_fields_param {
226 0     0     my ($self, $value) = @_;
227 0           my @columns;
228              
229 0 0         if (ref $value eq 'ARRAY') {
230 0           @columns = @$value;
231             }
232             else {
233 0           @columns = split /\s*,\s*/, $value;
234             }
235              
236 0           for my $clause (@columns) {
237             # we take care to avoid injection risks
238 0           my ($field) = ($clause =~ /^ ([a-z0-9_\.]*) $/x);
239 0 0         $self->throwable->throw_bad_request(400, errors => [{
240             parameter => "invalid fields clause",
241             _meta => { fields => $field, }, # XXX
242             }]) if not defined $field;
243             }
244              
245 0 0         $self->set( $self->set->search_rs(undef, { columns => \@columns }) )
246             if @columns;
247              
248 0           return;
249             }
250              
251              
252             sub _handle_sort_param {
253 0     0     my ($self, $value) = @_;
254 0           my @order_spec;
255              
256             # to support sort[typename]=... we need to be able to make type names
257             # to relationship names that map to the type and are included in the query
258             # (there might be more than one relationship on 'me' that leads to
259             # the same resource type so there's a potential ambiguity)
260 0 0         if (ref $value) {
261 0           $self->throwable->throw_bad_request(400, errors => [{
262             parameter => "per-type sort specifiers are not supported yet",
263             _meta => { sort => $value, }, # XXX
264             }]);
265             }
266              
267 0 0         if (not defined $value) {
268 0           $value = (join ",", map { "me.$_" } $self->set->result_source->primary_columns);
  0            
269             }
270              
271 0           for my $clause (split /,/, $value) {
272              
273             # we take care to avoid injection risks
274 0           my ($field, $dir);
275 0 0         if ($clause =~ /^ ([a-z0-9_\.]*)\b (?:\s+(asc|desc))? $/xi) {
    0          
276 0   0       ($field, $dir) = ($1, $2 || 'asc');
277             }
278             elsif ($clause =~ /^ (-?) ([a-z0-9_\.]*)$/xi) {
279 0 0         ($field, $dir) = ($2, ($1) ? 'desc' : 'asc');
280             }
281              
282 0 0         unless (defined $field) {
283 0           $self->throwable->throw_bad_request(400, errors => [{
284             parameter => "invalid order clause",
285             _meta => { order => $clause, }, # XXX
286             }]);
287             }
288              
289             # https://metacpan.org/pod/SQL::Abstract#ORDER-BY-CLAUSES
290 0           push @order_spec, { "-$dir" => $field };
291             }
292              
293 0 0         $self->set( $self->set->search_rs(undef, { order_by => \@order_spec }) )
294             if @order_spec;
295              
296 0           return;
297             }
298              
299              
300             sub _handle_distinct_param {
301 0     0     my ($self, $value) = @_;
302 0           my @errors;
303              
304             # these restrictions avoid edge cases we don't want to deal with yet
305 0   0       my $sort = $self->param('sort') || $self->param('order'); # XXX insufficient
306 0 0         push @errors, "distinct param requires sort (or order) param"
307             unless $sort;
308 0 0         push @errors, "distinct param requires fields param"
309             unless $self->param('fields');
310 0 0         push @errors, "distinct param requires fields and orders parameters to have same value"
311             unless $self->param('fields') eq $sort;
312 0           my $errors = join(", ", @errors);
313 0 0         die "$errors\n" if $errors; # TODO throw?
314              
315 0           $self->set( $self->set->search_rs(undef, { distinct => $value }) );
316              
317 0           return;
318             }
319              
320              
321              
322             1;
323              
324             __END__
325              
326             =pod
327              
328             =encoding UTF-8
329              
330             =head1 NAME
331              
332             WebAPI::DBIC::Resource::Role::DBICParams
333              
334             =head1 VERSION
335              
336             version 0.004001
337              
338             =head1 NAME
339              
340             WebAPI::DBIC::Resource::Role::DBICParams - methods for handling url parameters
341              
342             =head1 AUTHOR
343              
344             Tim Bunce <Tim.Bunce@pobox.com>
345              
346             =head1 COPYRIGHT AND LICENSE
347              
348             This software is copyright (c) 2015 by Tim Bunce.
349              
350             This is free software; you can redistribute it and/or modify it under
351             the same terms as the Perl 5 programming language system itself.
352              
353             =cut