File Coverage

blib/lib/CGI/Application/Plugin/Output/XSV.pm
Criterion Covered Total %
statement 104 108 96.3
branch 62 64 96.8
condition 29 30 96.6
subroutine 10 10 100.0
pod 4 4 100.0
total 209 216 96.7


line stmt bran cond sub pod time code
1             package CGI::Application::Plugin::Output::XSV;
2              
3 14     14   426292 use strict;
  14         33  
  14         539  
4 14     14   77 use warnings;
  14         31  
  14         509  
5              
6 14     14   75 use Carp;
  14         26  
  14         22394  
7             require Text::CSV_XS;
8             require Exporter;
9              
10             our @ISA= qw(Exporter);
11              
12             our @EXPORT= qw(
13             xsv_report_web
14             );
15              
16             our @EXPORT_OK= qw(
17             add_to_xsv
18             clean_field_names
19             xsv_report
20             );
21              
22             our %EXPORT_TAGS= (
23             all => [ @EXPORT, @EXPORT_OK ],
24             );
25              
26             our $VERSION= '1.02';
27              
28             ##
29              
30             sub xsv_report {
31 48   100 48 1 41208 my $args = shift || {};
32              
33 48 100       172 croak "argument to xsv_report must be a hash reference"
34             if ref( $args ) ne 'HASH';
35              
36 47         364 my %defaults = (
37             headers => undef,
38             headers_cb => \&clean_field_names,
39             include_headers => 1,
40             fields => undef,
41             values => undef,
42             row_filter => undef,
43             iterator => undef,
44             line_ending => "\n",
45             csv_opts => {},
46             maximum_iters => 1_000_000, # XXX reasonable default?
47             stream => 0,
48             );
49              
50 47         345 my %opts = ( %defaults, %$args );
51              
52 47         154 my $was_buffering = $|;
53 47         161 local $| = $was_buffering;
54 47 100       160 $| = 1 if $opts{stream};
55              
56             # deprecated option
57 47 100       130 if ( $opts{get_row_cb} ) {
58 2 100       6 if ( $opts{row_filter} ) {
59 1         8 carp "ignoring use of deprecated get_row_cb when row_filter specified";
60             }
61             else {
62 1         2 $opts{row_filter} = $opts{get_row_cb};
63 1         9 carp "get_row_cb is deprecated, please use row_filter instead";
64             }
65             }
66              
67 47 100 100     1545 croak "need array reference of values or iterator to do anything"
      100        
      66        
68             if ! ( $opts{values} && ref( $opts{values} ) eq 'ARRAY' )
69             && ! ( $opts{iterator} && ref( $opts{iterator} ) eq 'CODE' );
70              
71 43 100 100     219 croak "can't supply both values and iterator"
72             if $opts{values} && $opts{iterator};
73              
74             # list of fields to include in report
75 42         78 my $fields = [];
76              
77 42 100       148 if ( $opts{fields} ) {
    100          
78             # user-specified
79 21         39 $fields = $opts{fields};
80             }
81             elsif ( $opts{values} ) {
82             # try to determine field names from provided values
83 16 100       25 if ( @{ $opts{values} } ) {
  16         49  
84 15         36 my $list_type = ref( $opts{values}[0] );
85              
86             # field list from first entry in value list
87 15 100       73 if ( $list_type eq 'HASH' ) {
    100          
88 2         4 $fields = [ keys %{ $opts{values}[0] } ];
  2         10  
89             }
90             # or simply array indices
91             elsif ( $list_type eq 'ARRAY' ) {
92 12         31 $fields = [ 0..$#{$opts{values}[0]} ];
  12         40  
93             }
94             else {
95 1         18 croak "unknown list type [$list_type]";
96             }
97             }
98             else {
99 1         10 croak "can't determine field names (values is an empty list), aborting";
100             }
101             }
102             else {
103             # using iterator, empty field list
104             }
105              
106             # function to filter each row of data from $opts{values}
107 40         68 my $row_filter;
108              
109 40 100 100     166 if ( $opts{row_filter} ) {
  27 100       108  
110             # user-specified
111 7         9 $row_filter = $opts{row_filter};
112             }
113             elsif ( $opts{values} && @{ $opts{values} } ) {
114             # simple defaults for slices
115 24         59 my $list_type = ref( $opts{values}[0] );
116              
117 24 100       69 if ( $list_type eq 'HASH' ) {
    100          
118 13     19   71 $row_filter = sub { my ($row, $fields)= @_; return [ @$row{@$fields} ] };
  19         27  
  19         80  
119             }
120             elsif ( $list_type eq 'ARRAY' ) {
121 10     7   47 $row_filter = sub { my ($row, $fields)= @_; return [ @$row[@$fields] ] };
  7         11  
  7         25  
122             }
123             else {
124 1         12 croak "unknown list type [$list_type]";
125             }
126             }
127             else {
128             # using iterator, no filter
129 9     20   93 $row_filter = sub { $_[0] };
  20         69  
130             }
131              
132 39         231 my $csv = Text::CSV_XS->new( $opts{csv_opts} );
133 39         2410 my $output = '';
134              
135 39 100       112 if ( $opts{include_headers} ) {
136 31 100       89 if ( ! $opts{headers} ) {
137 22 100 100     151 if ( ! ($opts{headers_cb} && ref( $opts{headers_cb} ) eq 'CODE') ) {
  20 100       70  
138 2         21 croak "need headers or headers_cb to include headers";
139             }
140             elsif ( ! @{$fields} ) {
141 1         25 carp "passing empty fields list to headers_cb";
142             }
143             }
144              
145             # formatted column headers
146 29 100 100     744 my $readable_headers = $opts{headers} || $opts{headers_cb}->( $fields )
147             or croak "can't generate headers";
148              
149 28 100       186 croak "return value from headers_cb is not an array reference, aborting"
150             if ref ( $readable_headers ) ne 'ARRAY';
151              
152 27         76 $output .= add_to_xsv( $csv, $readable_headers, $opts{line_ending} );
153              
154 27 100       312 if ( $opts{stream} ) {
155 1         22 print $output;
156 1         3 $output = '';
157             }
158             }
159              
160 35 100       127 if ( $opts{values} ) {
161 27         39 foreach my $list_ref ( @{ $opts{values} } ) {
  27         91  
162 31         72 $output .= add_to_xsv(
163             $csv, $row_filter->($list_ref, $fields), $opts{line_ending}
164             );
165              
166 31 50       304 if ( $opts{stream} ) {
167 0         0 print $output;
168 0         0 $output = '';
169             }
170             }
171             }
172             # using iterator
173             else {
174 8         41 my $iterations = 0;
175              
176 8         32 while ( my $list_ref = $opts{iterator}->($fields) ) {
177 27 100       224 croak "return value from iterator is not an array reference, aborting"
178             if ref( $list_ref ) ne 'ARRAY';
179              
180             # XXX infinite loop?
181 26 100       87 croak "iterator exceeded maximum iterations ($opts{maximum_iters})"
182             if ++$iterations > $opts{maximum_iters};
183              
184 25         59 $output .= add_to_xsv(
185             $csv, $row_filter->($list_ref, $fields), $opts{line_ending}
186             );
187              
188 25 100       839 if ( $opts{stream} ) {
189 6         10 print $output;
190 6         18 $output = '';
191             }
192             }
193             }
194              
195 33         515 return $output;
196             }
197              
198             # send xsv output directly to browser for download
199             # same params as xsv_report, plus
200             # filename => 'download.csv',
201             sub xsv_report_web {
202 9     9 1 40494 my ($self, $args) = @_;
203 9   100     41 $args ||= {};
204              
205 9 100       42 croak "argument to xsv_report_web must be a hash reference"
206             if ref( $args ) ne 'HASH';
207              
208 8         21 my %defaults = (
209             filename => 'download.csv',
210             );
211              
212 8         37 my %opts = ( %defaults, %$args );
213              
214 8         43 my %headers = (
215             -type => 'application/x-csv',
216             '-content-disposition' => "attachment; filename=$opts{filename}",
217             );
218              
219             # we're doing our own output
220 8 50       26 if ( $opts{stream} ) {
221 0         0 $self->header_type('none');
222 0         0 print $self->query->header( %headers );
223             }
224             else {
225 8         39 $self->header_props( %headers );
226             }
227              
228             # consider use of magic goto in case of croak() inside xsv_report
229 8         279 return xsv_report( \%opts );
230             }
231              
232             # default field name generator:
233             # underscores to spaces, upper case first letter of each word
234             sub clean_field_names {
235 20     20 1 2516 my $fields = shift;
236              
237             # using temp var to avoid modifying $fields
238 20         27 my @fields_copy = @{$fields};
  20         53  
239              
240             return [
241 20         53 map { tr/_/ /; s/\b(\w+)/\u$1/g; $_ } @fields_copy
  50         91  
  50         307  
  50         201  
242             ];
243             }
244              
245             sub add_to_xsv {
246 90     90 1 5026 my ($csv, $fields, $line_ending) = @_;
247 90 100 100     475 croak "add_to_xsv: fields argument (required) must be an array reference"
248             if ! ($fields && ref( $fields ) eq 'ARRAY');
249              
250             # XXX redundant for empty string (or 0)
251 88   100     194 $line_ending ||= '';
252              
253 88 100       102 return $line_ending if ! @{$fields};
  88         276  
254              
255 87 100       118 $csv->combine( @{$fields} )
  87         361  
256 1         74 or croak "Failed to add [@{$fields}] to csv: " . $csv->error_input();
257              
258 86         2370 return $csv->string() . $line_ending;
259             }
260              
261             1;
262              
263             __END__