File Coverage

blib/lib/JQuery/DataTables/Request.pm
Criterion Covered Total %
statement 134 144 93.0
branch 74 94 78.7
condition 28 36 77.7
subroutine 17 17 100.0
pod 8 8 100.0
total 261 299 87.2


line stmt bran cond sub pod time code
1             package JQuery::DataTables::Request;
2              
3 4     4   112683 use 5.012;
  4         16  
  4         166  
4 4     4   21 use strict;
  4         8  
  4         120  
5 4     4   26 use warnings;
  4         14  
  4         150  
6              
7             our $VERSION = '0.109'; # VERSION
8              
9 4     4   35 use Carp;
  4         7  
  4         467  
10              
11 4     4   24 use base 'Class::Accessor';
  4         7  
  4         4028  
12              
13             =head1 NAME
14              
15             JQuery::DataTables::Request - represents a DataTables server-side request
16              
17             =head1 SYNOPSIS
18              
19             my $dt_req = JQuery::DataTables::Request->new( client_params => $client_parameters );
20             if ( $dt_req->column(0)->{searchable} ) {
21             # do something
22             }
23              
24             $dt_req->search->{value}; # the global search value
25             if ($dt_req->search->{regex}) {
26             # global search is set to regex
27             }
28              
29             # find the column definition with the name 'col_name'
30             my $cols = $dt_req->find_columns( by_name => 'col_name' );
31              
32             $dt_req->draw; #sEcho or draw parameter
33             $dt_req->start; #iDisplayStart or start parameter
34              
35             =head1 DESCRIPTION
36              
37             This module represents a DataTables server-side request originating from the DataTables
38             client side JS library. There are two major versions of DataTables(v1.9 and v1.10) that send
39             differently named parameters server-side for processing. This module only provides an API
40             that corresponds to the v1.10 parameters but maps the v1.9 parameters to the corresponding v1.10
41             parameters.
42              
43             The DataTable parameters are documented at the following locations:
44              
45             =over
46              
47             =item L
48              
49             =item L
50              
51             =back
52              
53             Each column parameter is represented as a HashRef like so:
54              
55             {
56             name => 'col_name',
57             data => 'col_name',
58             orderable => 1,
59             searchable => 1,
60             search => {
61             value => 'search string',
62             regex => 0,
63             }
64             }
65              
66             e.g.
67              
68             $dt_req->column(0)->{search}{value}
69              
70             Order parameters look like this:
71              
72             {
73             dir => 'asc',
74             column => 1
75             }
76              
77             e.g.
78              
79             $dt_req->order(0)->{dir}
80              
81             The order and column accessors are indexed the same way as your column parameters so
82             C<< $req->column(0) >> returns the column in the client_params C<[columns][0]> column.
83              
84             C is similar in that C<< $req->order(0) >> returns the C parameter data.
85              
86             =head1 METHODS
87              
88             =cut
89              
90             # V1.10 accessors
91             __PACKAGE__->mk_accessors(qw(
92             draw
93             start
94             length
95             search
96             _version
97             _order
98             _columns
99             )
100             );
101              
102             =head2 new
103              
104             Creates a new JQuery::DataTables::Request object.
105              
106             my $dt_request = JQuery::DataTables::Request->new( client_params => \%parameters );
107              
108             Accepts the following parameters
109              
110             =over
111              
112             =item client_params
113              
114             This is a HashRef that should contain your DataTables parameters as provided by the DataTables
115             JS library. Any parameters provided that are not recognized as DataTables request are silently ignored.
116             Usually, whatever framework you are using will already have a way to convert these parameters
117             to a HashRef for you, (e.g. C<< $c->req->parameters >> in a Catalyst app)
118              
119             =back
120              
121             new will confess/croak on the following scenarios:
122              
123             =over
124              
125             =item client_params is not provided
126              
127             =item client_params is not a HashRef
128              
129             =item client_params isn't recognized as containing DataTables parameters
130              
131             =back
132              
133             You should catch these if you are worried about it.
134              
135             =cut
136              
137             # client_params should be hashref
138             sub new {
139 6     6 1 1983 my ($class, %options) = @_;
140              
141 6 50       24 confess 'No DataTables parameters provided in the constructor - see client_params option'
142             unless defined($options{'client_params'});
143            
144 6 50       26 confess 'client_params must be a HashRef'
145             unless ref($options{'client_params'}) eq 'HASH';
146              
147 6         18 my $obj = bless {}, __PACKAGE__;
148              
149 6         26 my $version = $obj->version( $options{client_params} );
150 6 100 100     50 if (defined $version && $version eq '1.10') {
    100 66        
151 3         17 $obj->_process_v1_10_params( $options{'client_params'} );
152             } elsif (defined $version && $version eq '1.9') {
153 2         10 $obj->_process_v1_9_params( $options{'client_params'} );
154             } else {
155 1         238 confess 'client_params provided do not contain DataTables server-side parameters (i.e. this is not DataTables request data)';
156             }
157 5         37 $obj->_version( $version );
158 5         52 return $obj;
159             }
160              
161             =head2 column
162              
163             my \%column = $request->column(0);
164              
165             Returns a single column definition of the requested index
166              
167             =cut
168              
169             sub column {
170 6     6 1 1246 my ($self,$idx_arr) = @_;
171 6 50       17 return if !defined($idx_arr);
172 6         17 return $self->_columns->[$idx_arr];
173             }
174              
175             =head2 columns
176              
177             my \@columns = $request->columns([0,1]);
178              
179             Returns column definitions for the requested indexes. Can accept either an
180             arrayref of scalars or a single column scalar. If no column index is provided
181             all columns are returned.
182              
183             =cut
184              
185             sub columns {
186 9     9 1 968 my ($self, $idx_arr) = @_;
187 9         25 my $col_ref = $self->_columns;
188 9 100       109 return $col_ref if !defined($idx_arr);
189            
190 3 100       19 $idx_arr = [ $idx_arr ] if ref($idx_arr) ne 'ARRAY';
191              
192 3         6 my $ret_arr;
193 3         13 foreach my $idx ( sort @$idx_arr ) {
194 5         21 push(@$ret_arr, $col_ref->[$idx]);
195             }
196 3         15 return $ret_arr;
197             }
198              
199             =head2 columns_hashref
200              
201             Get all column definitions as a Hashref, with the column index as the key
202              
203             =cut
204              
205             sub columns_hashref {
206 1     1 1 3 my ($self) = @_;
207 1         2 my %col_hash;
208 1         2 @col_hash{ 0 .. $#{$self->_columns} } = @{$self->_columns};
  1         12  
  1         4  
209 1         18 return \%col_hash;
210             }
211              
212             =head2 find_columns
213              
214             $request->find_columns( %options )
215              
216             where C<%options> hash accepts the following parameters:
217              
218             =over
219              
220             =item by_name
221              
222             by_name accepts a scalar or arrayref of values and returns an arrayref of
223             column definitions
224              
225             my \@columns = $request->find_columns( by_name => ['col_name','col_name2'] );
226              
227             Searchs the columns C and/or C parameter.
228              
229             =item search_field
230              
231             my \@columns = $request->find_columns( by_name => 'something', search_field => 'name' );
232              
233             Set to either C or C to search those respective fields when
234             doing a C seach. If no search_field is specified, C searches
235             that match either field will be returned (i.e. defaults to search both fields)
236              
237             =item by_idx
238              
239             my \@columns = $request->find_columns( by_idx => $col_idx )
240              
241             This is just a passthrough to C<< $request->columns( $col_idx ); >>
242              
243             =back
244              
245             =cut
246              
247             sub find_columns {
248 3     3 1 942 my ($self, %options) = @_;
249 3 50       9 return unless %options;
250              
251 3 100       9 if (defined($options{by_idx})) {
252 1         7 return $self->columns($options{by_idx});
253             }
254              
255 2 50       8 if (my $searches = $options{by_name}) {
256 2         3 my $ret_cols;
257 2         3 my $key = $options{search_field};
258 2 50       8 $searches = [ $searches ] if ref($searches) ne 'ARRAY';
259 2         7 my $col_ref = $self->_columns;
260              
261 2         26 foreach my $search_val ( @$searches ) {
262 2         3 foreach my $col ( @$col_ref ) {
263 2 50       6 if ( defined $key ) {
264 0 0       0 if ( $col->{$key} eq $search_val ) {
265 0         0 push(@$ret_cols, $col);
266             }
267             } else {
268 2 50 33     8 if ( $col->{name} eq $search_val || $col->{data} eq $search_val ) {
269 2         8 push(@$ret_cols, $col);
270             }
271             }
272             }
273             }
274 2         41 return $ret_cols;
275             }
276             }
277              
278             =head2 order
279              
280             $req->order(0)->{dir}
281              
282             Returns the order data at provided index.
283              
284             =cut
285              
286             sub order
287             {
288 5     5 1 2151 my ($self,$idx) = @_;
289 5 50       12 return unless defined($idx);
290 5         14 return $self->_order->[$idx];
291             }
292              
293             =head2 orders
294              
295             $req->orders([0,1]);
296              
297             Returns an arrayref of the order data records at the provided indexes. Accepts an arrayref or scalar.
298             C<< ->orders([0,1]) >> will get C and C data.
299              
300             =cut
301              
302             sub orders
303             {
304 2     2 1 343 my ($self,$ar_idx) = @_;
305 2         6 my $ord_ref = $self->_order;
306 2 50       25 return $ord_ref unless defined($ar_idx);
307              
308 0 0       0 $ar_idx = [ $ar_idx ] unless ref($ar_idx) eq 'ARRAY';
309              
310 0         0 my $ret_arr;
311 0         0 foreach my $idx ( @$ar_idx ) {
312 0         0 push(@$ret_arr, $ord_ref->[$idx]);
313             }
314              
315 0         0 return $ret_arr;
316             }
317              
318             =head2 version
319              
320             my $version = $request->version( \%client_params? )
321              
322             Returns the version of DataTables we need to support based on the parameters sent.
323             v1.9 version of DataTables sends different named parameters than v1.10. Returns a string
324             of '1.9' if we think we have a 1.9 request, '1.10' if we think it is a 1.10 request or C
325             if we dont' think it is a DataTables request at all.
326              
327             This can be invoked as a class method as well as an instance method.
328              
329             =cut
330              
331             sub version
332             {
333 13     13 1 484 my ($self,$client_params) = @_;
334              
335 13 100 66     50 if (!ref($self) && !defined($client_params)) {
336 1         5 return;
337             }
338              
339 12 100       33 return $self->_version unless $client_params;
340 10         13 my $ref = $client_params;
341              
342             # v1.10 parameters
343 10 100 100     70 if (defined $ref->{draw} && defined $ref->{start} && defined $ref->{'length'}) {
      66        
344 4         19 return '1.10';
345             }
346              
347             # v1.9 parameters
348 6 50 66     41 if (defined $ref->{sEcho} && defined $ref->{iDisplayStart} && defined $ref->{iDisplayLength}) {
      66        
349 3         11 return '1.9';
350             }
351              
352 3         9 return;
353             }
354              
355             =head1 PRIVATE METHODS
356              
357              
358             =head2 _process_v1_9_params
359              
360             Processes v1.9 parameters, mapping them to 1.10 parameters
361              
362             $self->_process_v1_9_params( \%client_params )
363              
364             where C<\%client_params> is a HashRef containing the v1.9 parameters that DataTables
365             client library sends the server in server-side mode.
366              
367             =cut
368            
369             # maps 1.9 to 1.10 variables
370             # only thing not mapped is iColumns
371             my $vmap = {
372             top => {
373             'iDisplayStart' => 'start',
374             'iDisplayLength' => 'length',
375             'sEcho' => 'draw',
376             },
377             col_and_order => {
378             'bSearchable' => ['columns', 'searchable', undef],
379             'sSearch' => ['columns', 'search', 'value'],
380             'bRegex' => ['columns', 'search', 'regex'],
381             'bSortable' => ['columns', 'orderable', undef],
382             'mDataProp' => ['columns', 'data', undef],
383             'iSortCol' => ['order', 'column', undef],
384             'sSortDir' => ['order', 'dir', undef]
385             }
386             };
387              
388             sub _process_v1_9_params {
389 2     2   4 my ($self, $client_params) = @_;
390 2         5 my $columns;
391             my $order;
392 0         0 my $search;
393              
394 2         13 while ( my ($name,$val) = each %$client_params ) {
395             # handle top level parameters
396 28 100       120 if ( grep { $_ eq $name && $val =~ m/^[0-9]+$/ } keys %{$vmap->{top}} ) {
  84 100       347  
  28 100       68  
    100          
    100          
397 6         15 my $acc = $vmap->{top}->{$name};
398 6         42 $self->$acc( $val );
399             } elsif ($name eq 'sSearch') {
400 2         19 $search->{value} = $val;
401             } elsif ($name eq 'bRegex') {
402 2 50       13 $search->{regex} = $val eq 'true' ? 1 : 0;
403             } elsif ($name =~ m/^(?bSearchable|sSearch|bRegex|bSortable|iSortCol|sSortDir|mDataProp)_(?\d+)$/) {
404 4     4   37725 my $map = $vmap->{col_and_order}->{$+{param}};
  4         2298  
  4         3813  
  14         94  
405 14         69 my ($param,$idx,$sub_param1,$sub_param2,$new_val) =
406             $self->_validate_and_convert( $map->[0], $+{idx}, $map->[1], $map->[2], $val);
407              
408 14 100       51 if ($map->[0] eq 'columns') {
    50          
409 10 100       40 if (defined($sub_param2)) {
410 4         25 $columns->{$idx}{$sub_param1}{$sub_param2} = $new_val;
411             } else {
412 6         15 $columns->{$idx}{$sub_param1} = $new_val;
413             # copy name => data for v1.9 so that find_columns works as expected
414             # not really sure how to do this, Data::Alias? alias it eventually
415             # right now just copy
416 6 100       37 if ($sub_param1 eq 'data') {
417 2         12 $columns->{$idx}{'name'} = $new_val;
418             }
419             }
420             } elsif ($map->[0] eq 'order') {
421 4         25 $order->{$idx}{$sub_param1} = $new_val;
422             }
423             }
424             }
425              
426 2         5 my @col_arr;
427 2         12 push(@col_arr, $columns->{$_}) for ( sort keys %$columns );
428              
429 2         4 my @order_arr;
430 2         9 push(@order_arr, $order->{$_}) for ( sort keys %$order );
431              
432 2         9 $self->_columns( \@col_arr );
433 2         25 $self->_order( \@order_arr );
434 2         20 $self->search( $search );
435             }
436              
437             =head2 _process_v1_10_params
438              
439             $self->_process_v1_10_params( \%client_params );
440              
441             where C<\%client_params> is a HashRef containing the v1.10 parameters that DataTables
442             client library sends the server in server-side mode.
443              
444             =cut
445              
446             sub _process_v1_10_params {
447 3     3   8 my ($self, $client_params) = @_;
448              
449 3         6 my $columns;
450             my $order;
451 0         0 my $search;
452 3         19 while ( my ($name,$val) = each %$client_params ) {
453 47 100       69 $self->$name( $val ) if ( grep { $_ eq $name && $val =~ m/^[0-9]+$/ } qw(draw start length) );
  141 100       518  
454              
455 47 100       409 if ($name =~ m/^(?columns|order)\[(?[0-9]+)\]\[(?[^]]+)\](\[(?[^]]+)\])?$/) {
    100          
456 32         305 my ($param,$idx,$sub_param1,$sub_param2,$new_val) =
457             $self->_validate_and_convert($+{param}, $+{idx}+0, $+{sub_param1}, $+{sub_param2}, $val);
458              
459 32 100       151 if ($param eq 'columns') {
    50          
460 24 100       43 if (defined($sub_param2)) {
461 8         45 $columns->{$idx}{$sub_param1}{$sub_param2} = $new_val;
462             } else {
463 16         91 $columns->{$idx}{$sub_param1} = $new_val;
464             }
465             } elsif ($param eq 'order') {
466 8         16020 $order->{$idx}{$sub_param1} = $new_val;
467             }
468             } elsif ($name =~ m/^search\[(?regex|value)\]$/) {
469 6         42 my $sp = $+{search_param};
470 6 100       23 if ($sp eq 'regex') {
471 3 100       23 $search->{$sp} = $val eq 'true' ? 1 : 0;
472             } else {
473 3         33 $search->{$sp} = $val;
474             }
475             }
476             }
477              
478 3         43 my @col_arr;
479 3         31 push(@col_arr, $columns->{$_}) for ( sort keys %$columns );
480              
481 3         7 my @order_arr;
482 3         16 push(@order_arr, $order->{$_}) for ( sort keys %$order );
483              
484 3         17 $self->_columns( \@col_arr );
485 3         51 $self->_order( \@order_arr );
486 3         36 $self->search( $search );
487 3         34 return $self;
488             }
489              
490             =head2 _validate_and_convert
491              
492             Validates parameters are set properly and does boolean conversion
493              
494             =cut
495              
496             #XXX: make this not a mess
497             sub _validate_and_convert
498             {
499 46     46   248 my ($self,$param,$idx,$sub1,$sub2,$val) = @_;
500 46 100       173 if ($param eq 'columns') {
    50          
501 34 100 100     212 if ($sub1 eq 'orderable' || $sub1 eq 'searchable') {
    100 100        
502 12 100       35 $val = lc $val eq 'true' ? 1 : 0;
503             } elsif ( $sub1 eq 'search' && $sub2 eq 'regex' ) {
504 6 100       22 $val = lc $val eq 'true' ? 1 : 0;
505             }
506             } elsif ($param eq 'order') {
507 12 50 100     67 if ($sub1 eq 'dir' && lc $val ne 'asc' && lc $val ne 'desc') {
      66        
508             #warn 'Unknown order[dir] value provided. Must be asc or desc, defaulting to asc';
509 0         0 $val = 'asc';
510             }
511             }
512 46         168 return ($param,$idx,$sub1,$sub2,$val);
513             }
514              
515             =head1 AUTHOR
516              
517             Mike Wisener Exmikew_cpan_orgE
518              
519             =head1 COPYRIGHT AND LICENSE
520              
521             Copyright E 2014 by Mike Wisener
522              
523             This library is free software; you can redistribute it and/or modify
524             it under the same terms as Perl itself.
525              
526             =cut
527              
528             1;