File Coverage

blib/lib/Catalyst/Controller/DBIC/API/RequestArguments.pm
Criterion Covered Total %
statement 69 73 94.5
branch 26 30 86.6
condition 31 40 77.5
subroutine 19 19 100.0
pod n/a
total 145 162 89.5


line stmt bran cond sub pod time code
1             package Catalyst::Controller::DBIC::API::RequestArguments;
2             $Catalyst::Controller::DBIC::API::RequestArguments::VERSION = '2.008001';
3             #ABSTRACT: Provides Request argument validation
4 16     16   21487 use MooseX::Role::Parameterized;
  16         981905  
  16         79  
5 16     16   607875 use Catalyst::Controller::DBIC::API::Types(':all');
  16         48  
  16         185  
6 16     16   83728 use MooseX::Types::Moose(':all');
  16         45  
  16         139  
7 16     16   136191 use Scalar::Util('reftype');
  16         47  
  16         1253  
8 16     16   116 use Data::Dumper;
  16         44  
  16         859  
9 16     16   10230 use Catalyst::Controller::DBIC::API::Validator;
  16         110  
  16         776  
10 16     16   161 use namespace::autoclean;
  16         38  
  16         148  
11              
12 16     16   11023 use Catalyst::Controller::DBIC::API::JoinBuilder;
  16         76  
  16         43777  
13              
14              
15              
16              
17             has [qw( search_validator select_validator )] => (
18             is => 'ro',
19             isa => 'Catalyst::Controller::DBIC::API::Validator',
20             lazy => 1,
21             builder => '_build_validator',
22             );
23              
24             sub _build_validator {
25 135     135   5693 return Catalyst::Controller::DBIC::API::Validator->new;
26             }
27              
28             parameter static => ( isa => Bool, default => 0 );
29              
30             role {
31             my $p = shift;
32              
33             if ( $p->static ) {
34             requires
35             qw( check_has_relation check_column_relation prefetch_allows );
36             }
37             else {
38             requires qw( _controller check_has_relation check_column_relation );
39             }
40              
41              
42             has 'count' => (
43             is => 'ro',
44             writer => '_set_count',
45             isa => Int,
46             predicate => 'has_count',
47             );
48              
49              
50             has 'page' => (
51             is => 'ro',
52             writer => '_set_page',
53             isa => Int,
54             predicate => 'has_page',
55             );
56              
57              
58             has 'offset' => (
59             is => 'ro',
60             writer => '_set_offset',
61             isa => Int,
62             predicate => 'has_offset',
63             );
64              
65              
66             has 'ordered_by' => (
67             is => 'ro',
68             writer => '_set_ordered_by',
69             isa => OrderedBy,
70             predicate => 'has_ordered_by',
71             coerce => 1,
72             default => sub { $p->static ? [] : undef },
73             );
74              
75              
76             has 'grouped_by' => (
77             is => 'ro',
78             writer => '_set_grouped_by',
79             isa => GroupedBy,
80             predicate => 'has_grouped_by',
81             coerce => 1,
82             default => sub { $p->static ? [] : undef },
83             );
84              
85              
86             has prefetch => (
87             is => 'ro',
88             writer => '_set_prefetch',
89             isa => Prefetch,
90             default => sub { $p->static ? [] : undef },
91             coerce => 1,
92             trigger => sub {
93             my ( $self, $new ) = @_;
94              
95             foreach my $pf (@$new) {
96             if ( HashRef->check($pf) ) {
97             die
98             qq|'${\Dumper($pf)}' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
99             unless $self->prefetch_validator->validate($pf)->[0];
100             }
101             else {
102             die
103             qq|'$pf' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
104             unless $self->prefetch_validator->validate(
105             { $pf => 1 } )->[0];
106             }
107             }
108             },
109             );
110              
111              
112             has 'search_exposes' => (
113             is => 'ro',
114             writer => '_set_search_exposes',
115             isa => ArrayRef [ Str | HashRef ],
116             predicate => 'has_search_exposes',
117             default => sub { [] },
118             trigger => sub {
119             my ( $self, $new ) = @_;
120             $self->search_validator->load($_) for @$new;
121             },
122             );
123              
124              
125             has 'search' => (
126             is => 'ro',
127             writer => '_set_search',
128             isa => SearchParameters,
129             predicate => 'has_search',
130             coerce => 1,
131             trigger => sub {
132             my ( $self, $new ) = @_;
133              
134             if ( $self->has_search_exposes and @{ $self->search_exposes } ) {
135             foreach my $foo (@$new) {
136             while ( my ( $k, $v ) = each %$foo ) {
137             local $Data::Dumper::Terse = 1;
138             die
139             qq|{ $k => ${\Dumper($v)} } is not an allowed search term in: ${\join("\n", @{$self->search_validator->templates})}|
140             unless $self->search_validator->validate(
141             { $k => $v } )->[0];
142             }
143             }
144             }
145             else {
146             foreach my $foo (@$new) {
147             while ( my ( $k, $v ) = each %$foo ) {
148             $self->check_column_relation( { $k => $v } );
149             }
150             }
151             }
152              
153             my ( $search_parameters, $search_attributes ) =
154             $self->generate_parameters_attributes($new);
155             $self->_set_search_parameters($search_parameters);
156             $self->_set_search_attributes($search_attributes);
157              
158             },
159             );
160              
161              
162             has search_parameters => (
163             is => 'ro',
164             isa => SearchParameters,
165             writer => '_set_search_parameters',
166             predicate => 'has_search_parameters',
167             coerce => 1,
168             default => sub { [ {} ] },
169             );
170              
171              
172             has search_attributes => (
173             is => 'ro',
174             isa => HashRef,
175             writer => '_set_search_attributes',
176             predicate => 'has_search_attributes',
177             lazy_build => 1,
178             );
179              
180              
181             has search_total_entries => (
182             is => 'ro',
183             isa => Int,
184             writer => '_set_search_total_entries',
185             predicate => 'has_search_total_entries',
186             );
187              
188              
189             has 'select_exposes' => (
190             is => 'ro',
191             writer => '_set_select_exposes',
192             isa => ArrayRef [ Str | HashRef ],
193             predicate => 'has_select_exposes',
194             default => sub { [] },
195             trigger => sub {
196             my ( $self, $new ) = @_;
197             $self->select_validator->load($_) for @$new;
198             },
199             );
200              
201              
202             has select => (
203             is => 'ro',
204             writer => '_set_select',
205             isa => SelectColumns,
206             predicate => 'has_select',
207             default => sub { $p->static ? [] : undef },
208             coerce => 1,
209             trigger => sub {
210             my ( $self, $new ) = @_;
211             if ( $self->has_select_exposes ) {
212             foreach my $val (@$new) {
213             die "'$val' is not allowed in a select"
214             unless $self->select_validator->validate($val);
215             }
216             }
217             else {
218             $self->check_column_relation( $_, $p->static ) for @$new;
219             }
220             },
221             );
222              
223              
224             has as => (
225             is => 'ro',
226             writer => '_set_as',
227             isa => AsAliases,
228             default => sub { $p->static ? [] : undef },
229             trigger => sub {
230             my ( $self, $new ) = @_;
231             if ( $self->has_select ) {
232             die
233             "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
234             unless @$new == @{ $self->select || [] };
235             }
236             elsif ( defined $new ) {
237             die "'as' is only valid if 'select is also provided'";
238             }
239             }
240             );
241              
242              
243             has joins => (
244             is => 'ro',
245             isa => JoinBuilder,
246             lazy_build => 1,
247             handles => { build_joins => 'joins', }
248             );
249              
250              
251             has 'request_data' => (
252             is => 'ro',
253             isa => HashRef,
254             writer => '_set_request_data',
255             predicate => 'has_request_data',
256             trigger => sub {
257             my ( $self, $new ) = @_;
258             my $controller = $self->_controller;
259             return unless defined($new) && keys %$new;
260             $self->_set_prefetch( $new->{ $controller->prefetch_arg } )
261             if exists $new->{ $controller->prefetch_arg };
262             $self->_set_select( $new->{ $controller->select_arg } )
263             if exists $new->{ $controller->select_arg };
264             $self->_set_as( $new->{ $controller->as_arg } )
265             if exists $new->{ $controller->as_arg };
266             $self->_set_grouped_by( $new->{ $controller->grouped_by_arg } )
267             if exists $new->{ $controller->grouped_by_arg };
268             $self->_set_ordered_by( $new->{ $controller->ordered_by_arg } )
269             if exists $new->{ $controller->ordered_by_arg };
270             $self->_set_count( $new->{ $controller->count_arg } )
271             if exists $new->{ $controller->count_arg };
272             $self->_set_page( $new->{ $controller->page_arg } )
273             if exists $new->{ $controller->page_arg };
274             $self->_set_offset( $new->{ $controller->offset_arg } )
275             if exists $new->{ $controller->offset_arg };
276             $self->_set_search( $new->{ $controller->search_arg } )
277             if exists $new->{ $controller->search_arg };
278             }
279             );
280              
281             method _build_joins => sub {
282 55     55   709 return Catalyst::Controller::DBIC::API::JoinBuilder->new(
        55      
283             name => 'TOP' );
284             };
285              
286              
287             method format_search_parameters => sub {
288 32     32   97 my ( $self, $params ) = @_;
        32      
289              
290 32         90 my $genparams = [];
291              
292 32         90 foreach my $param (@$params) {
293 32         139 push(
294             @$genparams,
295             $self->generate_column_parameters(
296             $self->stored_result_source,
297             $param, $self->joins
298             )
299             );
300             }
301              
302 31         1417 return $genparams;
303             };
304              
305              
306             method generate_column_parameters => sub {
307 85     85   1260 my ( $self, $source, $param, $join, $base ) = @_;
        85      
308 85   100     325 $base ||= 'me';
309 85         185 my $search_params = {};
310              
311             # return non-hashref params unaltered
312 85 100       340 return $param
313             unless ref $param eq 'HASH';
314              
315             # build up condition
316 55         232 foreach my $column ( keys %$param ) {
317 60         156 my $value = $param->{$column};
318 60 100 100     236 if ( $source->has_relationship($column) ) {
    100 66        
    100          
319              
320             # check if the value isn't a hashref
321 19 100       177 unless ( ref $value eq 'HASH' )
322             {
323 2         11 $search_params->{ join( '.', $base, $column ) } =
324             $value;
325 2         9 next;
326             }
327              
328             $search_params = {
329             %$search_params,
330 17         50 %{ $self->generate_column_parameters(
  17         76  
331             $source->related_source($column),
332             $value,
333             Catalyst::Controller::DBIC::API::JoinBuilder->new(
334             parent => $join,
335             name => $column
336             ),
337             $column
338             )
339             }
340             };
341             }
342             elsif ( $source->has_column($column) ) {
343             $search_params->{ join( '.', $base, $column ) } =
344 27         589 $param->{$column};
345             }
346             elsif ( $column eq '-or' || $column eq '-and' || $column eq '-not' ) {
347             # either an arrayref or hashref
348 11 50       206 if ( ref $value eq 'HASH' ) {
    50          
349 0         0 $search_params->{$column} = $self->generate_column_parameters(
350             $source,
351             $value,
352             $join,
353             $base,
354             );
355             }
356             elsif ( ref $value eq 'ARRAY' ) {
357 36         108 push @{$search_params->{$column}},
358             $self->generate_column_parameters(
359             $source,
360             $_,
361             $join,
362             $base,
363             )
364 11         36 for @$value;
365             }
366             else {
367 0         0 die "unsupported value '$value' for column '$column'\n";
368             }
369             }
370              
371             # might be a sql function instead of a column name
372             # e.g. {colname => {like => '%foo%'}}
373             else {
374             # but only if it's not a hashref
375 3 100       76 unless ( ref $value eq 'HASH' ) {
376             $search_params->{ join( '.', $base, $column ) } =
377 2         15 $param->{$column};
378             }
379             else {
380 1         15 die "unsupported value '$value' for column '$column'\n";
381             }
382             }
383             }
384              
385 53         290 return $search_params;
386             };
387              
388              
389             method generate_parameters_attributes => sub {
390 32     32   118 my ( $self, $args ) = @_;
        32      
391              
392 32         163 return ( $self->format_search_parameters($args),
393             $self->search_attributes );
394             };
395              
396             method _build_search_attributes => sub {
397 54     54   184 my ( $self, $args ) = @_;
        54      
398 54         6610 my $static = $self->_controller;
399             my $search_attributes = {
400             group_by => $self->grouped_by
401             || (
402             ( scalar( @{ $static->grouped_by } ) ) ? $static->grouped_by
403             : undef
404             ),
405             order_by => $self->ordered_by
406             || (
407             ( scalar( @{ $static->ordered_by } ) ) ? $static->ordered_by
408             : undef
409             ),
410             select => $self->select
411             || (
412             ( scalar( @{ $static->select } ) ) ? $static->select
413             : undef
414             ),
415             as => $self->as
416 54   100     2161 || ( ( scalar( @{ $static->as } ) ) ? $static->as : undef ),
      100        
      100        
      66        
      50        
      100        
417             prefetch => $self->prefetch || $static->prefetch || undef,
418             rows => $self->count || $static->count,
419             page => $static->page,
420             offset => $self->offset,
421             join => $self->build_joins,
422             };
423              
424 54 100 33     2419 if ( $self->has_page ) {
    50 33        
425 4         158 $search_attributes->{page} = $self->page;
426             }
427             elsif (!$self->has_page
428             && defined( $search_attributes->{offset} )
429             && defined( $search_attributes->{rows} ) )
430             {
431             $search_attributes->{page} =
432 0         0 $search_attributes->{offset} / $search_attributes->{rows} + 1;
433 0         0 delete $search_attributes->{offset};
434             }
435              
436             $search_attributes = {
437 172         595 map {@$_}
438             grep {
439             defined( $_->[1] )
440             ? ( ref( $_->[1] )
441             && reftype( $_->[1] ) eq 'HASH'
442             && keys %{ $_->[1] } )
443             || ( ref( $_->[1] )
444             && reftype( $_->[1] ) eq 'ARRAY'
445 486 100 66     2200 && @{ $_->[1] } )
446             || length( $_->[1] )
447             : undef
448             }
449 54         304 map { [ $_, $search_attributes->{$_} ] }
  486         1183  
450             keys %$search_attributes
451             };
452              
453 54 100 100     414 if ( $search_attributes->{page} && !$search_attributes->{rows} ) {
454 1         12 die 'list_page can only be used with list_count';
455             }
456              
457 53 100       191 if ( $search_attributes->{select} ) {
458              
459             # make sure all columns have an alias to avoid ambiguous issues
460             # but allow non strings (eg. hashrefs for db procs like 'count')
461             # to pass through unmolested
462             $search_attributes->{select} = [
463 20 100 66     4395 map { ( Str->check($_) && $_ !~ m/\./ ) ? "me.$_" : $_ }
464             ( ref $search_attributes->{select} )
465 14         48 ? @{ $search_attributes->{select} }
466             : $search_attributes->{select}
467 14 50       56 ];
468             }
469              
470 53         12184 return $search_attributes;
471              
472             };
473              
474             };
475              
476              
477             1;
478              
479             __END__
480              
481             =pod
482              
483             =head1 NAME
484              
485             Catalyst::Controller::DBIC::API::RequestArguments - Provides Request argument validation
486              
487             =head1 VERSION
488              
489             version 2.008001
490              
491             =head1 DESCRIPTION
492              
493             RequestArguments embodies those arguments that are provided as part of a request
494             or effect validation on request arguments. This Role can be consumed in one of
495             two ways. As this is a parameterized Role, it accepts a single argument at
496             composition time: 'static'. This indicates that those parameters should be
497             stored statically and used as a fallback when the current request doesn't
498             provide them.
499              
500             =head1 PUBLIC_ATTRIBUTES
501              
502             =head2 count
503              
504             The number of rows to be returned during paging.
505              
506             =head2 page
507              
508             What page to return while paging.
509              
510             =head2 offset
511              
512             Specifies where to start the paged result (think SQL LIMIT).
513              
514             =head2 ordered_by
515              
516             Is passed to ->search to determine sorting.
517              
518             =head2 grouped_by
519              
520             Is passed to ->search to determine aggregate results.
521              
522             =head2 prefetch
523              
524             Is passed to ->search to optimize the number of database fetches for joins.
525              
526             =head2 search_exposes
527              
528             Limits what can actually be searched. If a certain column isn't indexed or
529             perhaps a BLOB, you can explicitly say which columns can be search to exclude
530             that one.
531              
532             Like the synopsis in DBIC::API shows, you can declare a "template" of what is
533             allowed (by using '*'). Each element passed in, will be converted into a
534             Data::DPath and added to the validator.
535              
536             =head2 search
537              
538             Contains the raw search parameters. Upon setting, a trigger will fire to format
539             them, set search_parameters and search_attributes.
540              
541             Please see L</generate_parameters_attributes> for details on how the format works.
542              
543             =head2 search_parameters
544              
545             Stores the formatted search parameters that will be passed to ->search.
546              
547             =head2 search_attributes
548              
549             Stores the formatted search attributes that will be passed to ->search.
550              
551             =head2 search_total_entries
552              
553             Stores the total number of entries in a paged search result.
554              
555             =head2 select_exposes
556              
557             Limits what can actually be selected. Use this to whitelist database functions
558             (such as COUNT).
559              
560             Like the synopsis in DBIC::API shows, you can declare a "template" of what is
561             allowed (by using '*'). Each element passed in, will be converted into a
562             Data::DPath and added to the validator.
563              
564             =head2 select
565              
566             Is the search attribute that allows you to both limit what is returned in the
567             result set and also make use of database functions like COUNT.
568              
569             Please see L<DBIx::Class::ResultSet/select> for more details.
570              
571             =head2 as
572              
573             Is the search attribute compliment to L</select> that allows you to label
574             columns for object inflaction and actually reference database functions like
575             COUNT.
576              
577             Please see L<DBIx::Class::ResultSet/as> for more details.
578              
579             =head2 joins
580              
581             Holds the top level JoinBuilder object used to keep track of joins automagically
582             while formatting complex search parameters.
583              
584             Provides the method 'build_joins' which returns the 'join' attribute for
585             search_attributes.
586              
587             =head2 request_data
588              
589             Holds the raw (but deserialized) data for this request.
590              
591             =head1 PRIVATE_ATTRIBUTES
592              
593             =head2 search_validator
594              
595             A Catalyst::Controller::DBIC::API::Validator instance used solely to validate
596             search parameters.
597              
598             =head2 select_validator
599              
600             A Catalyst::Controller::DBIC::API::Validator instance used solely to validate
601             select parameters.
602              
603             =head2 prefetch_validator
604              
605             A Catalyst::Controller::DBIC::API::Validator instance used solely to validate
606             prefetch parameters.
607              
608             =head1 PROTECTED_METHODS
609              
610             =head2 format_search_parameters
611              
612             Iterates through the provided arrayref calling generate_column_parameters on
613             each one.
614              
615             =head2 generate_column_parameters
616              
617             Recursively generates properly aliased parameters for search building a new
618             JoinBuilder each layer of recursion.
619              
620             =head2 generate_parameters_attributes
621              
622             Takes the raw search arguments and formats them by calling
623             format_search_parameters. Then builds the related attributes, preferring
624             request-provided arguments for things like grouped_by over statically configured
625             options. Finally tacking on the appropriate joins.
626              
627             Returns a list of both formatted search parameters and attributes.
628              
629             =head1 AUTHORS
630              
631             =over 4
632              
633             =item *
634              
635             Nicholas Perez <nperez@cpan.org>
636              
637             =item *
638              
639             Luke Saunders <luke.saunders@gmail.com>
640              
641             =item *
642              
643             Alexander Hartmaier <abraxxa@cpan.org>
644              
645             =item *
646              
647             Florian Ragwitz <rafl@debian.org>
648              
649             =item *
650              
651             Oleg Kostyuk <cub.uanic@gmail.com>
652              
653             =item *
654              
655             Samuel Kaufman <sam@socialflow.com>
656              
657             =back
658              
659             =head1 COPYRIGHT AND LICENSE
660              
661             This software is copyright (c) 2019 by Luke Saunders, Nicholas Perez, Alexander Hartmaier, et al.
662              
663             This is free software; you can redistribute it and/or modify it under
664             the same terms as the Perl 5 programming language system itself.
665              
666             =cut