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