improve data_root & item_root documentation
[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
4cb8623a 446 # return non-hashref params unaltered
447 return $param
448 unless ref $param eq 'HASH';
449
d2739840 450 # build up condition
8ea592cb 451 foreach my $column ( keys %$param ) {
4cb8623a 452 my $value = $param->{$column};
8ea592cb 453 if ( $source->has_relationship($column) ) {
454
11ba2ccc 455 # check if the value isn't a hashref
4cb8623a 456 unless ( ref $value eq 'HASH' )
d2739840 457 {
8ea592cb 458 $search_params->{ join( '.', $base, $column ) } =
4cb8623a 459 $value;
d2739840 460 next;
461 }
462
8ea592cb 463 $search_params = {
464 %$search_params,
465 %{ $self->generate_column_parameters(
466 $source->related_source($column),
4cb8623a 467 $value,
8ea592cb 468 Catalyst::Controller::DBIC::API::JoinBuilder->new(
469 parent => $join,
470 name => $column
471 ),
472 $column
473 )
474 }
475 };
d2739840 476 }
8ea592cb 477 elsif ( $source->has_column($column) ) {
478 $search_params->{ join( '.', $base, $column ) } =
479 $param->{$column};
d2739840 480 }
4cb8623a 481 elsif ( $column eq '-or' || $column eq '-and' || $column eq '-not' ) {
482 # either an arrayref or hashref
483 if ( ref $value eq 'HASH' ) {
484 $search_params->{$column} = $self->generate_column_parameters(
485 $source,
486 $value,
487 $join,
488 $base,
489 );
490 }
491 elsif ( ref $value eq 'ARRAY' ) {
492 push @{$search_params->{$column}},
493 $self->generate_column_parameters(
494 $source,
495 $_,
496 $join,
497 $base,
498 )
499 for @$value;
500 }
501 else {
502 die "unsupported value '$value' for column '$column'\n";
503 }
504 }
8ea592cb 505
11ba2ccc 506 # might be a sql function instead of a column name
507 # e.g. {colname => {like => '%foo%'}}
8ea592cb 508 else {
11ba2ccc 509 # but only if it's not a hashref
4cb8623a 510 unless ( ref $value eq 'HASH' ) {
8ea592cb 511 $search_params->{ join( '.', $base, $column ) } =
512 $param->{$column};
11ba2ccc 513 }
514 else {
4cb8623a 515 die "unsupported value '$value' for column '$column'\n";
11ba2ccc 516 }
517 }
d2739840 518 }
519
520 return $search_params;
521 };
522
523=method_protected generate_parameters_attributes
524
c0c8e1c6 525Takes the raw search arguments and formats them by calling
526format_search_parameters. Then builds the related attributes, preferring
527request-provided arguments for things like grouped_by over statically configured
528options. Finally tacking on the appropriate joins.
529
530Returns a list of both formatted search parameters and attributes.
d2739840 531
532=cut
533
8ea592cb 534 method generate_parameters_attributes => sub {
535 my ( $self, $args ) = @_;
d2739840 536
8ea592cb 537 return ( $self->format_search_parameters($args),
538 $self->search_attributes );
d2739840 539 };
540
8ea592cb 541 method _build_search_attributes => sub {
542 my ( $self, $args ) = @_;
543 my $static = $self->_controller;
544 my $search_attributes = {
545 group_by => $self->grouped_by
546 || (
547 ( scalar( @{ $static->grouped_by } ) ) ? $static->grouped_by
548 : undef
549 ),
550 order_by => $self->ordered_by
551 || (
552 ( scalar( @{ $static->ordered_by } ) ) ? $static->ordered_by
553 : undef
554 ),
555 select => $self->select
556 || (
557 ( scalar( @{ $static->select } ) ) ? $static->select
558 : undef
559 ),
560 as => $self->as
561 || ( ( scalar( @{ $static->as } ) ) ? $static->as : undef ),
d2739840 562 prefetch => $self->prefetch || $static->prefetch || undef,
8ea592cb 563 rows => $self->count || $static->count,
564 page => $static->page,
565 offset => $self->offset,
566 join => $self->build_joins,
d2739840 567 };
568
8ea592cb 569 if ( $self->has_page ) {
33003023 570 $search_attributes->{page} = $self->page;
571 }
8ea592cb 572 elsif (!$self->has_page
573 && defined( $search_attributes->{offset} )
574 && defined( $search_attributes->{rows} ) )
33003023 575 {
8ea592cb 576 $search_attributes->{page} =
577 $search_attributes->{offset} / $search_attributes->{rows} + 1;
fa2501f0 578 delete $search_attributes->{offset};
33003023 579 }
33003023 580
8ea592cb 581 $search_attributes = {
582 map {@$_}
583 grep {
584 defined( $_->[1] )
585 ? ( ref( $_->[1] )
586 && reftype( $_->[1] ) eq 'HASH'
587 && keys %{ $_->[1] } )
588 || ( ref( $_->[1] )
589 && reftype( $_->[1] ) eq 'ARRAY'
590 && @{ $_->[1] } )
591 || length( $_->[1] )
592 : undef
593 }
594 map { [ $_, $search_attributes->{$_} ] }
595 keys %$search_attributes
d2739840 596 };
597
8ea592cb 598 if ( $search_attributes->{page} && !$search_attributes->{rows} ) {
d2739840 599 die 'list_page can only be used with list_count';
600 }
406086f3 601
8ea592cb 602 if ( $search_attributes->{select} ) {
603
d2739840 604 # make sure all columns have an alias to avoid ambiguous issues
605 # but allow non strings (eg. hashrefs for db procs like 'count')
606 # to pass through unmolested
8ea592cb 607 $search_attributes->{select} = [
608 map { ( Str->check($_) && $_ !~ m/\./ ) ? "me.$_" : $_ }
609 ( ref $search_attributes->{select} )
610 ? @{ $search_attributes->{select} }
611 : $search_attributes->{select}
612 ];
d2739840 613 }
614
615 return $search_attributes;
406086f3 616
d2739840 617 };
618
619};
8ea592cb 620
d2739840 621=head1 DESCRIPTION
622
8ea592cb 623RequestArguments embodies those arguments that are provided as part of a request
624or effect validation on request arguments. This Role can be consumed in one of
625two ways. As this is a parameterized Role, it accepts a single argument at
626composition time: 'static'. This indicates that those parameters should be
627stored statically and used as a fallback when the current request doesn't
628provide them.
d2739840 629
630=cut
631
d2739840 6321;