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