perltidy all classes
[catagits/Catalyst-Controller-DBIC-API.git] / lib / Catalyst / Controller / DBIC / API / RequestArguments.pm
CommitLineData
d2739840 1package Catalyst::Controller::DBIC::API::RequestArguments;
2
3#ABSTRACT: Provides Request argument validation
4use MooseX::Role::Parameterized;
5use Catalyst::Controller::DBIC::API::Types(':all');
6use MooseX::Types::Moose(':all');
7use Scalar::Util('reftype');
8use Data::Dumper;
28098e5d 9use Catalyst::Controller::DBIC::API::Validator;
d2739840 10use namespace::autoclean;
11
12use Catalyst::Controller::DBIC::API::JoinBuilder;
13
d2739840 14=attribute_private search_validator
15
16A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters
17
18=cut
19
d2739840 20=attribute_private select_validator
21
22A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters
23
24=cut
25
d2739840 26=attribute_private prefetch_validator
27
28A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters
29
30=cut
31
4e5983f2 32has [qw( search_validator select_validator )] => (
8ea592cb 33 is => 'ro',
34 isa => 'Catalyst::Controller::DBIC::API::Validator',
35 lazy => 1,
28098e5d 36 builder => '_build_validator',
37);
38
39sub _build_validator {
40 return Catalyst::Controller::DBIC::API::Validator->new;
41}
d2739840 42
43parameter static => ( isa => Bool, default => 0 );
44
45role {
d2739840 46 my $p = shift;
406086f3 47
8ea592cb 48 if ( $p->static ) {
49 requires
50 qw( check_has_relation check_column_relation prefetch_allows );
d2739840 51 }
8ea592cb 52 else {
53 requires qw( _controller check_has_relation check_column_relation );
d2739840 54 }
55
56=attribute_public count is: ro, isa: Int
57
58count is the number of rows to be returned during paging
59
60=cut
61
8ea592cb 62 has 'count' => (
63 is => 'ro',
64 writer => '_set_count',
65 isa => Int,
d2739840 66 predicate => 'has_count',
67 );
68
69=attribute_public page is: ro, isa: Int
70
71page is what page to return while paging
72
73=cut
74
8ea592cb 75 has 'page' => (
76 is => 'ro',
77 writer => '_set_page',
78 isa => Int,
d2739840 79 predicate => 'has_page',
80 );
81
33003023 82=attribute_public offset is ro, isa: Int
83
84offset specifies where to start the paged result (think SQL LIMIT)
85
86=cut
87
8ea592cb 88 has 'offset' => (
89 is => 'ro',
90 writer => '_set_offset',
91 isa => Int,
33003023 92 predicate => 'has_offset',
93 );
94
d2739840 95=attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy>
96
97ordered_by is passed to ->search to determine sorting
98
99=cut
100
8ea592cb 101 has 'ordered_by' => (
102 is => 'ro',
103 writer => '_set_ordered_by',
104 isa => OrderedBy,
d2739840 105 predicate => 'has_ordered_by',
8ea592cb 106 coerce => 1,
107 default => sub { $p->static ? [] : undef },
d2739840 108 );
109
110=attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy>
111
112grouped_by is passed to ->search to determine aggregate results
113
114=cut
115
8ea592cb 116 has 'grouped_by' => (
117 is => 'ro',
118 writer => '_set_grouped_by',
119 isa => GroupedBy,
d2739840 120 predicate => 'has_grouped_by',
8ea592cb 121 coerce => 1,
122 default => sub { $p->static ? [] : undef },
d2739840 123 );
124
125=attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch>
126
127prefetch is passed to ->search to optimize the number of database fetches for joins
128
129=cut
130
8ea592cb 131 has prefetch => (
132 is => 'ro',
133 writer => '_set_prefetch',
134 isa => Prefetch,
d2739840 135 default => sub { $p->static ? [] : undef },
8ea592cb 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})}|
4e5983f2 144 unless $self->prefetch_validator->validate($pf)->[0];
d2739840 145 }
8ea592cb 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];
d2739840 151 }
152 }
d2739840 153 },
154 );
155
156=attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef]
157
158search_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
160Like 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
8ea592cb 164 has 'search_exposes' => (
165 is => 'ro',
166 writer => '_set_search_exposes',
167 isa => ArrayRef [ Str | HashRef ],
d2739840 168 predicate => 'has_search_exposes',
8ea592cb 169 default => sub { [] },
170 trigger => sub {
171 my ( $self, $new ) = @_;
d2739840 172 $self->search_validator->load($_) for @$new;
173 },
174 );
175
d666a194 176=attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
d2739840 177
178search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
179
180Please see L</generate_parameters_attributes> for details on how the format works.
181
182=cut
183
8ea592cb 184 has 'search' => (
185 is => 'ro',
186 writer => '_set_search',
187 isa => SearchParameters,
d2739840 188 predicate => 'has_search',
8ea592cb 189 coerce => 1,
190 trigger => sub {
191 my ( $self, $new ) = @_;
406086f3 192
8ea592cb 193 if ( $self->has_search_exposes and @{ $self->search_exposes } ) {
194 foreach my $foo (@$new) {
195 while ( my ( $k, $v ) = each %$foo ) {
d2739840 196 local $Data::Dumper::Terse = 1;
8ea592cb 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];
d2739840 201 }
202 }
203 }
8ea592cb 204 else {
205 foreach my $foo (@$new) {
206 while ( my ( $k, $v ) = each %$foo ) {
207 $self->check_column_relation( { $k => $v } );
d2739840 208 }
209 }
210 }
406086f3 211
8ea592cb 212 my ( $search_parameters, $search_attributes ) =
213 $self->generate_parameters_attributes($new);
d2739840 214 $self->_set_search_parameters($search_parameters);
215 $self->_set_search_attributes($search_attributes);
216
217 },
218 );
219
d666a194 220=attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
d2739840 221
222search_parameters stores the formatted search parameters that will be passed to ->search
223
224=cut
225
8ea592cb 226 has search_parameters => (
227 is => 'ro',
228 isa => SearchParameters,
229 writer => '_set_search_parameters',
d2739840 230 predicate => 'has_search_parameters',
8ea592cb 231 coerce => 1,
232 default => sub { [ {} ] },
d2739840 233 );
234
235=attribute_public search_attributes is:ro, isa: HashRef
236
237search_attributes stores the formatted search attributes that will be passed to ->search
238
239=cut
240
8ea592cb 241 has search_attributes => (
242 is => 'ro',
243 isa => HashRef,
244 writer => '_set_search_attributes',
245 predicate => 'has_search_attributes',
d2739840 246 lazy_build => 1,
247 );
248
249=attribute_public search_total_entries is: ro, isa: Int
250
251search_total_entries stores the total number of entries in a paged search result
252
253=cut
254
8ea592cb 255 has search_total_entries => (
256 is => 'ro',
257 isa => Int,
258 writer => '_set_search_total_entries',
d2739840 259 predicate => 'has_search_total_entries',
260 );
261
262=attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef]
263
264select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
265
266Like 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
8ea592cb 270 has 'select_exposes' => (
271 is => 'ro',
272 writer => '_set_select_exposes',
273 isa => ArrayRef [ Str | HashRef ],
d2739840 274 predicate => 'has_select_exposes',
8ea592cb 275 default => sub { [] },
276 trigger => sub {
277 my ( $self, $new ) = @_;
d2739840 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
284select 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
286Please see L<DBIx::Class::ResultSet/select> for more details.
287
288=cut
289
8ea592cb 290 has select => (
291 is => 'ro',
292 writer => '_set_select',
293 isa => SelectColumns,
d2739840 294 predicate => 'has_select',
8ea592cb 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) {
d2739840 301 die "'$val' is not allowed in a select"
302 unless $self->select_validator->validate($val);
303 }
304 }
8ea592cb 305 else {
306 $self->check_column_relation( $_, $p->static ) for @$new;
d2739840 307 }
308 },
309 );
310
311=attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases>
312
313as 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
315Please see L<DBIx::Class::ResultSet/as> for more details.
316
317=cut
318
8ea592cb 319 has as => (
320 is => 'ro',
321 writer => '_set_as',
322 isa => AsAliases,
d2739840 323 default => sub { $p->static ? [] : undef },
8ea592cb 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 || [] };
d2739840 330 }
8ea592cb 331 elsif ( defined $new ) {
d2739840 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
339joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
340
341Provides a single handle which returns the 'join' attribute for search_attributes:
342
343 build_joins => 'joins'
344
345=cut
346
8ea592cb 347 has joins => (
348 is => 'ro',
349 isa => JoinBuilder,
d2739840 350 lazy_build => 1,
8ea592cb 351 handles => { build_joins => 'joins', }
d2739840 352 );
353
354=attribute_public request_data is: ro, isa: HashRef
355
356request_data holds the raw (but deserialized) data for ths request
357
358=cut
359
8ea592cb 360 has 'request_data' => (
361 is => 'ro',
362 isa => HashRef,
363 writer => '_set_request_data',
533075c7 364 predicate => 'has_request_data',
8ea592cb 365 trigger => sub {
366 my ( $self, $new ) = @_;
d2739840 367 my $controller = $self->_controller;
533075c7 368 return unless defined($new) && keys %$new;
8ea592cb 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 };
d2739840 387 }
388 );
389
8ea592cb 390 method _build_joins => sub {
391 return Catalyst::Controller::DBIC::API::JoinBuilder->new(
392 name => 'TOP' );
393 };
d2739840 394
395=method_protected format_search_parameters
396
397format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one
398
399=cut
400
8ea592cb 401 method format_search_parameters => sub {
402 my ( $self, $params ) = @_;
406086f3 403
d2739840 404 my $genparams = [];
405
8ea592cb 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 );
d2739840 414 }
415
416 return $genparams;
417 };
418
419=method_protected generate_column_parameters
420
421generate_column_parameters recursively generates properly aliased parameters for search, building a new JoinBuilder each layer of recursion
422
423=cut
424
8ea592cb 425 method generate_column_parameters => sub {
426 my ( $self, $source, $param, $join, $base ) = @_;
d2739840 427 $base ||= 'me';
02b625cd 428 my $search_params = {};
d2739840 429
430 # build up condition
8ea592cb 431 foreach my $column ( keys %$param ) {
432 if ( $source->has_relationship($column) ) {
433
11ba2ccc 434 # check if the value isn't a hashref
8ea592cb 435 unless ( ref( $param->{$column} )
436 && reftype( $param->{$column} ) eq 'HASH' )
d2739840 437 {
8ea592cb 438 $search_params->{ join( '.', $base, $column ) } =
439 $param->{$column};
d2739840 440 next;
441 }
442
8ea592cb 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 };
d2739840 456 }
8ea592cb 457 elsif ( $source->has_column($column) ) {
458 $search_params->{ join( '.', $base, $column ) } =
459 $param->{$column};
d2739840 460 }
8ea592cb 461
11ba2ccc 462 # might be a sql function instead of a column name
463 # e.g. {colname => {like => '%foo%'}}
8ea592cb 464 else {
11ba2ccc 465 # but only if it's not a hashref
8ea592cb 466 unless ( ref( $param->{$column} )
467 && reftype( $param->{$column} ) eq 'HASH' )
468 {
469 $search_params->{ join( '.', $base, $column ) } =
470 $param->{$column};
11ba2ccc 471 }
472 else {
473 die "$column is neither a relationship nor a column\n";
474 }
475 }
d2739840 476 }
477
478 return $search_params;
479 };
480
481=method_protected generate_parameters_attributes
482
483generate_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
8ea592cb 487 method generate_parameters_attributes => sub {
488 my ( $self, $args ) = @_;
d2739840 489
8ea592cb 490 return ( $self->format_search_parameters($args),
491 $self->search_attributes );
d2739840 492 };
493
494=method_protected _build_search_attributes
495
496This builder method generates the search attributes
497
498=cut
499
8ea592cb 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 ),
d2739840 521 prefetch => $self->prefetch || $static->prefetch || undef,
8ea592cb 522 rows => $self->count || $static->count,
523 page => $static->page,
524 offset => $self->offset,
525 join => $self->build_joins,
d2739840 526 };
527
8ea592cb 528 if ( $self->has_page ) {
33003023 529 $search_attributes->{page} = $self->page;
530 }
8ea592cb 531 elsif (!$self->has_page
532 && defined( $search_attributes->{offset} )
533 && defined( $search_attributes->{rows} ) )
33003023 534 {
8ea592cb 535 $search_attributes->{page} =
536 $search_attributes->{offset} / $search_attributes->{rows} + 1;
fa2501f0 537 delete $search_attributes->{offset};
33003023 538 }
33003023 539
8ea592cb 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
d2739840 555 };
556
8ea592cb 557 if ( $search_attributes->{page} && !$search_attributes->{rows} ) {
d2739840 558 die 'list_page can only be used with list_count';
559 }
406086f3 560
8ea592cb 561 if ( $search_attributes->{select} ) {
562
d2739840 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
8ea592cb 566 $search_attributes->{select} = [
567 map { ( Str->check($_) && $_ !~ m/\./ ) ? "me.$_" : $_ }
568 ( ref $search_attributes->{select} )
569 ? @{ $search_attributes->{select} }
570 : $search_attributes->{select}
571 ];
d2739840 572 }
573
574 return $search_attributes;
406086f3 575
d2739840 576 };
577
578};
8ea592cb 579
d2739840 580=head1 DESCRIPTION
581
8ea592cb 582RequestArguments embodies those arguments that are provided as part of a request
583or effect validation on request arguments. This Role can be consumed in one of
584two ways. As this is a parameterized Role, it accepts a single argument at
585composition time: 'static'. This indicates that those parameters should be
586stored statically and used as a fallback when the current request doesn't
587provide them.
d2739840 588
589=cut
590
d2739840 5911;