File Coverage

lib/Log/Shiras/Report/CSVFile.pm
Criterion Covered Total %
statement 189 192 98.4
branch 46 60 76.6
condition 5 6 83.3
subroutine 30 30 100.0
pod 1 4 25.0
total 271 292 92.8


line stmt bran cond sub pod time code
1             package Log::Shiras::Report::CSVFile;
2             our $AUTHORITY = 'cpan:JANDREW';
3 1     1   564 use version; our $VERSION = version->declare("v0.48.0");
  1         2  
  1         6  
4 1     1   72 use strict;
  1         1  
  1         19  
5 1     1   3 use warnings;
  1         1  
  1         21  
6 1     1   21 use 5.010;
  1         2  
7 1     1   5 use utf8;
  1         1  
  1         6  
8 1     1   19 use Moose;
  1         1  
  1         5  
9 1     1   4064 use namespace::autoclean;
  1         1  
  1         10  
10 1     1   58 use MooseX::StrictConstructor;
  1         1  
  1         6  
11 1     1   1830 use MooseX::HasDefaults::RO;
  1         1  
  1         7  
12 1     1   2982 use lib '../../../';
  1         1  
  1         7  
13             #~ use Log::Shiras::Unhide qw( :InternalReporTCSV );
14             ###InternalReporTCSV warn "You uncovered internal logging statements for Log::Shiras::Report::CSVFile-$VERSION" if !$ENV{hide_warn};
15             ###InternalReporTCSV use Log::Shiras::Switchboard;
16             ###InternalReporTCSV my $switchboard = Log::Shiras::Switchboard->instance;
17 1     1   985 use Text::CSV_XS 1.25;
  1         7187  
  1         45  
18 1     1   5 use File::Copy qw( copy );
  1         1  
  1         40  
19 1     1   3 use File::Temp;
  1         1  
  1         56  
20             #~ $File::Temp::DEBUG = 1;
21 1     1   4 use Carp qw( confess cluck );
  1         1  
  1         35  
22 1     1   4 use Fcntl qw( :flock LOCK_EX LOCK_UN SEEK_END);#
  1         1  
  1         135  
23 1         10 use MooseX::Types::Moose qw(
24             FileHandle ArrayRef HashRef Str Bool
25 1     1   4 );
  1         1  
26 1     1   3463 use Log::Shiras::Types qw( HeaderArray HeaderString CSVFile IOFileType );
  1         1  
  1         7  
27             with 'Log::Shiras::LogSpace';
28              
29             #########1 Public Attributes 3#########4#########5#########6#########7#########8#########9
30              
31             has file =>(
32             isa => CSVFile,
33             writer => 'set_file_name',
34             reader => 'get_file_name',
35             clearer => '_clear_file',
36             predicate => '_has_file',
37             required => 1,
38             coerce => 1,
39             );
40            
41             has headers =>(
42             isa => HeaderArray,
43             traits =>['Array'],
44             writer => 'set_headers',
45             reader => 'get_headers',
46             predicate => 'has_headers',
47             clearer => '_clear_headers',
48             handles =>{
49             number_of_headers => 'count',
50             },
51             coerce => 1,
52             );
53            
54             has reconcile_headers =>(
55             isa => Bool,
56             writer => 'set_reconcile_headers',
57             reader => 'should_reconcile_headers',
58             default => 1,
59             );
60              
61             has test_first_row =>(
62             isa => Bool,
63             writer => '_test_first_row',
64             reader => 'should_test_first_row',
65             default => 1,
66             );
67              
68             #########1 Public Methods 3#########4#########5#########6#########7#########8#########9
69              
70             sub add_line{
71              
72 7     7 1 910 my ( $self, $input_ref ) = @_;
73             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
74             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line' ),
75             ###InternalReporTCSV message =>[ 'Adding a line to the csv file -' . $self->get_file_name . '- :', $input_ref ], } );
76 7         8 my $message_ref;
77 7         7 my( $first_ref, @other_args ) = @{$input_ref->{message}};
  7         15  
78 7 50       24 if( !$first_ref ){
    100          
    50          
79             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
80             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_find_the_actual_message' ),
81             ###InternalReporTCSV message =>[ 'No data in the first position - adding an empty row' ], } );
82 0         0 $message_ref = $self->_build_message_from_arrayref( [] );
83             }elsif( @other_args ){
84             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
85             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_find_the_actual_message' ),
86             ###InternalReporTCSV message =>[ 'Multiple values passed - treating the inputs like a list' ], } );
87 3         13 $message_ref = $self->_build_message_from_arrayref( [ $first_ref, @other_args ] );
88             }elsif( is_HashRef( $first_ref ) ){
89             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
90             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_find_the_actual_message' ),
91             ###InternalReporTCSV message =>[ 'Using the ref as it stands:', $first_ref ], } );
92 4         503 $message_ref = $self->_build_message_from_hashref( $first_ref );
93             }else{
94             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 3,
95             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_find_the_actual_message' ),
96             ###InternalReporTCSV message =>[ 'Treating the input as a one element string' ], } );
97 0         0 $message_ref = $self->_build_message_from_arrayref( [ $first_ref ] );
98             }
99             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 3,
100             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line' ),
101             ###InternalReporTCSV message =>[ "committing the message:", $message_ref ], } );
102 7         157 $self->_send_array_ref( $self->_get_file_handle, $message_ref );
103            
104 7         289 return 1;
105             }
106              
107 5     5 0 9 sub get_class_space{ 'CSVFile' }
108              
109             #########1 Private Attributes 3#########4#########5#########6#########7#########8#########9
110              
111             has _file_handle =>(
112             isa => IOFileType,
113             writer => '_set_file_handle',
114             reader => '_get_file_handle',
115             clearer => '_clear_file_handle',
116             predicate => '_has_file_handle',
117             init_arg => undef,
118             );
119              
120             has _file_headers =>(
121             isa => HeaderArray,
122             traits =>['Array'],
123             writer => '_set_file_headers',
124             reader => '_get_file_headers',
125             clearer => '_clear_file_headers',
126             predicate => '_has_file_headers',
127             handles =>{
128             _file_header_count => 'count',
129             },
130             init_arg => undef,
131             );
132              
133             has _expected_header_lookup =>(
134             isa => HashRef,
135             traits =>['Hash'],
136             writer => '_set_header_lookup',
137             reader => '_get_header_lookup',
138             clearer => '_clear_header_lookup',
139             predicate => '_has_header_lookup',
140             handles =>{
141             _get_header_position => 'get',
142             _has_header_named => 'exists',
143             },
144             init_arg => undef,
145             );
146              
147             has _csv_parser =>(
148             isa => 'Text::CSV_XS',
149             writer => '_set_csv_parser',
150             clearer => '_clear_csv_parser',
151             init_arg => undef,
152             handles =>{
153             _set_parsing_header => 'header',
154             _send_array_ref => 'say',
155             _send_hash_ref => 'print_hr',
156             _read_next_line => 'getline',
157             _separator_char => 'sep_char',
158             },
159             );
160              
161             #########1 Private Methods 3#########4#########5#########6#########7#########8#########9
162              
163             sub BUILD{
164 5     5 0 107 my( $self, ) = @_;
165             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
166             ###InternalReporTCSV name_space => $self->get_all_space( 'BUILD' ),
167             ###InternalReporTCSV message =>[ "Organizing the new file instance"], } );
168            
169             # Open and collect the header if available
170 5         110 $self->_open_file( $self->get_file_name );
171             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
172             ###InternalReporTCSV name_space => $self->get_all_space( 'BUILD' ),
173             ###InternalReporTCSV message =>[ "Open file complete"], } );
174            
175             # Check requested headers against an empty file
176 5 100       104 if( $self->has_headers ){
177 2         40 my $header_ref = $self->get_headers;
178 2         14 $self->_set_expected_header_lookup( $header_ref );
179 2 100 66     46 if( $self->should_reconcile_headers and !$self->_has_file_headers ){
180             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
181             ###InternalReporTCSV name_space => $self->get_all_space( 'BUILD' ),
182             ###InternalReporTCSV message =>[ "Ensuring the requested headers are in the file:", $header_ref ], } );
183 1         4 $self->_add_headers_to_file( $header_ref );
184             }
185             }
186             #~ confess "Died here";
187 5         473 return 1;
188             }
189              
190             after 'set_file_name' => sub{ my( $self, $file ) = @_; $self->_open_file( $file ) };
191              
192             sub _open_file{
193              
194 11     11   14 my ( $self, $file ) = @_;
195             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
196             ###InternalReporTCSV name_space => $self->get_all_space( '_open_file' ),
197             ###InternalReporTCSV message =>[ "Arrived at _open_file for:", $file ], } );
198 11         260 $self->_clear_file_handle;
199 11         234 $self->_clear_file_headers;
200 11         224 $self->_clear_csv_parser;
201            
202             # Build the csv parser
203 11         70 $self->_set_csv_parser( Text::CSV_XS->new({ binary => 1, eol => $\, auto_diag => 1 }) );#
204            
205             # Open the file handle and collect the header if available
206 1 50   1   6 open( my $fh, "+<:encoding(UTF-8)", $file ) or confess "Can't open $file: $!";
  1         1  
  1         5  
  11         289  
207 11         8713 binmode( $fh );
208 11         45 flock( $fh, LOCK_EX );
209 11         301 $self->_set_file_handle( $fh );
210             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
211             ###InternalReporTCSV name_space => $self->get_all_space( '_open_file' ),
212             ###InternalReporTCSV message =>[ 'Read file handle built: ' . -s $fh ], } );
213            
214             # Collect the header if available
215 11 100       250 if( -s $self->_get_file_handle ){
216             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
217             ###InternalReporTCSV name_space => $self->get_all_space( '_open_file' ),
218             ###InternalReporTCSV message =>[ "The file appears to have pre-existing content (headers)" ], } );
219 7         5 my $header_ref;
220 7         24 @$header_ref = $self->_set_parsing_header( $fh );
221             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
222             ###InternalReporTCSV name_space => $self->get_all_space( '_open_file' ),
223             ###InternalReporTCSV message =>[ "File headers are: " . join( '~|~', @$header_ref ) ], } );
224 7         2104 $self->_set_file_headers( $header_ref );
225             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
226             ###InternalReporTCSV name_space => $self->get_all_space( '_open_file' ),
227             ###InternalReporTCSV message =>[ "File headers set" ], } );
228             }else{
229             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
230             ###InternalReporTCSV name_space => $self->get_all_space( '_open_file' ),
231             ###InternalReporTCSV message =>[ "The file is zero size" ], } );
232             }
233            
234             # Get to the end for add_line (in case you weren't there before)
235 11 50       233 seek( $self->_get_file_handle, 0, SEEK_END) or confess "Can't seek (end) on $file: $!";
236            
237 11         17 return 1;
238             }
239              
240             around '_set_file_headers' => sub{
241             my( $_set_file_headers, $self, $header_ref ) = @_;
242             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
243             ###InternalReporTCSV name_space => $self->get_all_space( '_set_file_headers' ),
244             ###InternalReporTCSV message =>[ 'Attempting to set the file headers to:', $header_ref ], } );
245             if( $self->should_reconcile_headers ){
246             my( $one_extra, $two_extra ) = $self->_test_headers( $header_ref, $self->get_headers );
247             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
248             ###InternalReporTCSV name_space => $self->get_all_space( '_set_file_headers' ),
249             ###InternalReporTCSV message =>[ 'Returned from the header test:', $one_extra, $two_extra ], } );
250             $self->set_reconcile_headers( 0 );
251             if( $two_extra ){
252             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 3,
253             ###InternalReporTCSV name_space => $self->get_all_space( '_set_file_headers' ),
254             ###InternalReporTCSV message =>[ 'There are more expected headers than were found in the file:', $two_extra ], } );
255             push @$header_ref, @$two_extra;
256             $self->_add_headers_to_file( $header_ref );
257             }
258             if( $one_extra ){
259             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 3,
260             ###InternalReporTCSV name_space => $self->get_all_space( '_set_file_headers' ),
261             ###InternalReporTCSV message =>[ 'There are more file headers than expected headers:', $one_extra ], } );
262             $self->set_headers( $header_ref );
263             }
264             $self->set_reconcile_headers( 1 );
265             }
266             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 3,
267             ###InternalReporTCSV name_space => $self->get_all_space( '_set_file_headers' ),
268             ###InternalReporTCSV message =>[ "Setting file headers to: ", $header_ref ], } );
269             $self->$_set_file_headers( $header_ref );
270             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
271             ###InternalReporTCSV name_space => $self->get_all_space( '_set_file_headers' ),
272             ###InternalReporTCSV message =>[ 'Final file headers:', $self->_get_file_headers ], } );
273             };
274              
275             around 'set_headers' => sub{
276             my( $set_headers_method, $self, $header_ref ) = @_;
277             $self->_clear_header_lookup;
278             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
279             ###InternalReporTCSV name_space => $self->get_all_space( 'set_headers' ),
280             ###InternalReporTCSV message =>[ 'Received a request to set headers to:', $header_ref ], } );
281             $header_ref = $self->_scrub_header_array( $header_ref );
282             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
283             ###InternalReporTCSV name_space => $self->get_all_space( 'set_headers' ),
284             ###InternalReporTCSV message =>[ 'Attempting to set the requested headers with:', $header_ref ], } );
285             $self->_set_expected_header_lookup( $header_ref );
286             my( $one_extra, $two_extra, $translation );
287             if( $self->should_reconcile_headers ){
288             my $file_headers = $self->_get_file_headers;
289             ( $one_extra, $two_extra, $translation ) = $self->_test_headers( $file_headers, $header_ref, );
290             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
291             ###InternalReporTCSV name_space => $self->get_all_space( '_set_file_headers' ),
292             ###InternalReporTCSV message =>[ 'Returned from the header test:', $one_extra, $two_extra, $translation ], } );
293             $self->set_reconcile_headers( 0 );
294             if( $two_extra ){
295             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 3,
296             ###InternalReporTCSV name_space => $self->get_all_space( '_set_file_headers' ),
297             ###InternalReporTCSV message =>[ 'There are more expected headers than were found in the file:', $two_extra ], } );
298             my $new_ref;
299             push @$new_ref, @$file_headers if $file_headers;
300             push @$new_ref, @$two_extra;
301             $self->_add_headers_to_file( $new_ref );
302             $header_ref = $new_ref;
303             }
304             $self->set_reconcile_headers( 1 );
305             }
306             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 3,
307             ###InternalReporTCSV name_space => $self->get_all_space( '_set_file_headers' ),
308             ###InternalReporTCSV message =>[ "Setting requested headers to: ", $header_ref ], } );
309             $self->$set_headers_method( $header_ref );
310             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
311             ###InternalReporTCSV name_space => $self->get_all_space( 'set_headers' ),
312             ###InternalReporTCSV message =>[ 'Final requested headers resolved to:', $header_ref,
313             ###InternalReporTCSV '...with passing-to translation resolved as:', $translation ], } );
314             return $translation;
315             };
316              
317             sub _add_headers_to_file{
318              
319 6     6   6 my ( $self, $new_ref ) = @_;
320             #~ my $new_line = join( $self->_separator_char, @$new_ref ) . "\n";
321             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
322             ###InternalReporTCSV name_space => $self->get_all_space( '_add_headers_to_file' ),
323             ###InternalReporTCSV message =>[ "Arrived at _add_headers_to_file for:", $new_ref, ], } );
324            
325             # Make a temp file to create new data
326 6         32 my $temp_dir = File::Temp->newdir( CLEANUP => 1 );
327 6         1513 my $fh = File::Temp->new( UNLINK => 0, DIR => $temp_dir );
328 6         1480 my $temp_parser = Text::CSV_XS->new({ binary => 1, sep_char => $self->_separator_char, eol => $\, auto_diag => 1 });#
329             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
330             ###InternalReporTCSV name_space => $self->get_all_space( '_add_headers_to_file' ),
331             ###InternalReporTCSV message =>[ "Tempfile open: " . $fh->filename, ], } );
332            
333             # Add the new header
334 6         606 $temp_parser->say( $fh, $new_ref );
335             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
336             ###InternalReporTCSV name_space => $self->get_all_space( '_add_headers_to_file' ),
337             ###InternalReporTCSV message =>[ "Added headers to the tempfile: ", $new_ref, ], } );
338            
339             # Write the rest of the lines (except the old header)
340 6         389 my $original_fh = $self->_get_file_handle;
341 6         131 $self->_clear_file_handle;
342 6         19 my $first_line = 1;
343 6         21 seek( $original_fh, 0, 0 );
344 6         24 while (my $row = $self->_read_next_line($original_fh)) {
345 5 100       223 if( $first_line ){
346 3         5 $first_line = 0;
347 3         9 next;
348             }
349             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
350             ###InternalReporTCSV name_space => $self->get_all_space( '_add_headers_to_file' ),
351             ###InternalReporTCSV message =>[ "Printing line to tempfile:", $row], } );
352 2         6 $temp_parser->say( $fh, $row );
353             }
354            
355             # Close the original file
356 6         415 flock( $original_fh, LOCK_UN );
357 6 50       46 close( $original_fh ) or confess "Couldn't close file: $!";
358             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
359             ###InternalReporTCSV name_space => $self->get_all_space( '_add_headers_to_file' ),
360             ###InternalReporTCSV message =>[ "Closed the original file handle" ], } );
361            
362             # Close the new tempfile
363 6         121 flock( $fh, LOCK_UN );
364 6         29 close( $fh );
365             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
366             ###InternalReporTCSV name_space => $self->get_all_space( '_add_headers_to_file' ),
367             ###InternalReporTCSV message =>[ "Closed the new temp file" ], } );
368            
369             # Replace the original file with the tempfile
370 6 50       20 copy( $fh->filename, $self->get_file_name ) or confess "Couldn't copy file: $!";
371             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
372             ###InternalReporTCSV name_space => $self->get_all_space( '_add_headers_to_file' ),
373             ###InternalReporTCSV message =>[ "Original file replaced: " . $self->get_file_name,
374             ###InternalReporTCSV '..with file: ' . $fh->filename ], } );
375 6         1020 $fh = undef;
376            
377             # Re-run the file to get the headers registered with Text::CSV_XS;
378 6         17 $self->_open_file( $self->get_file_name );
379             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
380             ###InternalReporTCSV name_space => $self->get_all_space( '_add_headers_to_file' ),
381             ###InternalReporTCSV message =>[ "Updated file re-test complete" ], } );
382            
383 6         40 return 1;
384             }
385              
386             sub _test_headers{
387              
388 6     6   7 my ( $self, $header_ref_1, $header_ref_2 ) = @_;
389             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
390             ###InternalReporTCSV name_space => $self->get_all_space( '_test_headers' ),
391             ###InternalReporTCSV message =>[ "Arrived at test headers with:", $header_ref_1, $header_ref_2 ], } );
392 6         6 my( $one_extra, $two_extra, $translation );
393 6 50       10 if( !$header_ref_2 ){
394             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 3,
395             ###InternalReporTCSV name_space => $self->get_all_space( '_test_headers' ),
396             ###InternalReporTCSV message =>[ "No second header list passed for testing" ], } );
397 0         0 $one_extra = $header_ref_1;
398             }else{
399 6         7 my $x = 0;
400 6         9 for my $second_header ( @$header_ref_2 ){
401             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
402             ###InternalReporTCSV name_space => $self->get_all_space( '_test_headers' ),
403             ###InternalReporTCSV message =>[ "Testing second header: $second_header" ], } );
404 25         15 my $y = 0;
405 25         17 my $found_match = 0;
406 25         20 NEWHEADERTEST: for my $first_header ( @$header_ref_1 ){
407             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
408             ###InternalReporTCSV name_space => $self->get_all_space( '_test_headers' ),
409             ###InternalReporTCSV message =>[ "Testing first header -$first_header- for a match" ], } );
410 43 100       53 if( $second_header eq $first_header ){
411             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
412             ###InternalReporTCSV name_space => $self->get_all_space( '_test_headers' ),
413             ###InternalReporTCSV message =>[ "Second header list -$second_header- at position: $x",
414             ###InternalReporTCSV "matches first header list header -$first_header- at position: $y" ], } );
415 12         16 $translation->{$x} = $y;
416 12         11 $found_match = 1;
417 12         12 last NEWHEADERTEST;
418             }
419 31         19 $y++;
420             }
421 25 100       38 push @$two_extra, $second_header if !$found_match;
422 25         23 $x++;
423             }
424 6         15 for my $pos ( 0 .. $#$header_ref_1 ){
425 15 100       23 if( !exists $translation->{$pos} ){
426 3         4 push @$one_extra, $header_ref_1->[$pos];
427             }
428             }
429 6         8 my $next_pos = $#$header_ref_1 + 1;
430 6         10 for my $pos ( 0 .. $#$header_ref_2 ){
431 25 100       34 if( !exists $translation->{$pos} ){
432 13         17 $translation->{$pos} = $next_pos++;
433             }
434             }
435             }
436             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
437             ###InternalReporTCSV name_space => $self->get_all_space( '_test_headers' ),
438             ###InternalReporTCSV message =>[ "Finished with header list 1 extra:", $one_extra,
439             ###InternalReporTCSV "...and header list 2 extra:", $two_extra,
440             ###InternalReporTCSV "...and translation ref:", $translation ], } );
441 6         12 return( $one_extra, $two_extra, $translation );
442             }
443              
444             sub _build_message_from_arrayref{
445 3     3   4 my( $self, $array_ref )= @_;
446             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
447             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_arrayref' ),
448             ###InternalReporTCSV message =>[ 'Testing the message from an array ref: ' . ($self->should_test_first_row//0), $array_ref ], } );
449 3 100       103 my @expected_headers = $self->has_headers ? @{$self->get_headers} : ();
  2         45  
450 3 100       71 if( $self->should_test_first_row ){
451             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
452             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_arrayref' ),
453             ###InternalReporTCSV message =>[ 'First row - testing if the list matches the header count' ], } );
454            
455 2 50       6 if( $#$array_ref != $#expected_headers ){
456 2 100       5 if( scalar( @expected_headers ) == 0 ){
457             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 3,
458             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_arrayref' ),
459             ###InternalReporTCSV message =>[ 'Adding dummy file headers' ], } );
460 1         2 my $dummy_headers;
461 1         3 map{ $dummy_headers->[$_] = "header_" . $_ } ( 0 .. $#$array_ref );
  6         10  
462             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
463             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_arrayref' ),
464             ###InternalReporTCSV message =>[ 'New dummy headers:', $dummy_headers ], } );
465 1 50       103 cluck "Setting dummy headers ( " . join( ', ', @$dummy_headers ) . " )" if !$ENV{hide_warn};
466 1         26 $self->set_reconcile_headers( 1 );
467 1         3 $self->set_headers( $dummy_headers );
468             }else{
469             cluck "The first added row has -" . scalar( @$array_ref ) .
470             "- items - but the report expects -" .
471 1 50       126 scalar( @expected_headers ) . "- items" if !$ENV{hide_warn};
472             }
473             }
474 2         51 $self->_test_first_row ( 0 );
475             }
476             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
477             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_arrayref' ),
478             ###InternalReporTCSV message =>[ 'Returning message ref:', $array_ref ], } );
479 3         8 return $array_ref;
480             }
481              
482             sub _build_message_from_hashref{
483 4     4   5 my( $self, $hash_ref )= @_;
484             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
485             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_hashref' ),
486             ###InternalReporTCSV message =>[ 'Building the array ref from the hash ref: ' . ($self->should_test_first_row//0), $hash_ref ], } );
487            
488             # Scrub the hash
489 4         4 my( $better_hash, @missing_list );
490 4         12 for my $key ( keys %$hash_ref ){
491 12         16 my $fixed_key = $self->_scrub_header_string( $key );
492             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
493             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_hashref' ),
494             ###InternalReporTCSV message =>[ "Managing key -$fixed_key- for key: $key" ], } );
495 12 100 100     265 push @missing_list, $fixed_key if $self->should_test_first_row and !$self->_has_header_named( $fixed_key );
496 12         25 $better_hash->{$fixed_key} = $hash_ref->{$key};
497             }
498 4         84 $self->_test_first_row( 0 );
499             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
500             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_hashref' ),
501             ###InternalReporTCSV message =>[ "Updated hash message:", $better_hash,
502             ###InternalReporTCSV "...with missing list:", @missing_list ], } );
503            
504             # Handle first row errors
505 4 100       8 if( @missing_list ){
506 2 100       41 my @expected_headers = $self->has_headers ? @{$self->get_headers} : ();
  1         20  
507 2         6 push @expected_headers, @missing_list;
508             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 3,
509             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_hashref' ),
510             ###InternalReporTCSV message =>[ "Updating the expected headers with new data", [@expected_headers] ], } );
511 2 50       216 cluck "Adding headers from the first hashref ( " . join( ', ', @missing_list ) . " )" if !$ENV{hide_warn};
512 2         51 $self->set_reconcile_headers( 1 );
513 2         8 $self->set_headers( [@expected_headers] );
514             }
515            
516             # Build the array_ref
517 4         7 my $array_ref = [];
518             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
519             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_hashref' ),
520             ###InternalReporTCSV message =>[ 'Building an array ref with loookup:', $self->_get_header_lookup ], } );
521 4         9 for my $header ( keys %$better_hash ){
522 12 100       306 if( $self->_has_header_named( $header ) ){
523 11         284 $array_ref->[$self->_get_header_position( $header )] = $better_hash->{$header};
524             }else{
525 1 50       112 cluck "found a hash key in the message that doesn't match the expected header ( $header )" if !$ENV{hide_warn};
526             }
527             }
528            
529             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
530             ###InternalReporTCSV name_space => $self->get_all_space( 'add_line::_build_message_from_hashref' ),
531             ###InternalReporTCSV message =>[ 'Returning message array ref:', $array_ref ], } );
532 4         12 return $array_ref;
533             }
534              
535             sub _set_expected_header_lookup{
536 6     6   8 my ( $self, $hash_ref ) = @_;
537             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
538             ###InternalReporTCSV name_space => $self->get_all_space( '_set_expected_header_lookup' ),
539             ###InternalReporTCSV message =>[ "Arrived at _set_expected_header_lookup with:", $hash_ref ], } );
540 6         8 my( $i, $positions, ) = ( 0, {} );
541 6         8 map{ $positions->{$_} = $i++ } @$hash_ref;
  25         34  
542             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
543             ###InternalReporTCSV name_space => $self->get_all_space( '_set_expected_header_lookup' ),
544             ###InternalReporTCSV message =>[ "Header lookup hash is:", $positions ], } );
545 6         147 $self->_set_header_lookup( $positions );
546             }
547              
548             sub _scrub_header_array{
549 4     4   6 my ( $self, @args ) = @_;
550             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
551             ###InternalReporTCSV name_space => $self->get_all_space( '_scrub_header_array' ),
552             ###InternalReporTCSV message =>[ "Arrived at _scrub_header_array:", @args ], } );
553 4         6 my $new_ref = [];
554 4         3 for my $header ( @{$args[0]} ){
  4         11  
555 17         30 push @$new_ref, $self->_scrub_header_string( $header );
556             }
557             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
558             ###InternalReporTCSV name_space => $self->get_all_space( '_scrub_header_array' ),
559             ###InternalReporTCSV message =>[ "Updated header is:", $new_ref ], } );
560 4         8 return $new_ref;
561             }
562              
563             sub _scrub_header_string{
564 29     29   23 my ( $self, $string ) = @_;
565             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
566             ###InternalReporTCSV name_space => $self->get_all_space( '_scrub_header_string' ),
567             ###InternalReporTCSV message =>[ "Arrived at _scrub_header_string with: $string" ], } );
568 29         32 $string = lc( $string );
569             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
570             ###InternalReporTCSV name_space => $self->get_all_space( '_scrub_header_string' ),
571             ###InternalReporTCSV message =>[ "The updated string is: $string" ], } );
572 29         25 $string =~ s/\n/ /gsxm;
573             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
574             ###InternalReporTCSV name_space => $self->get_all_space( '_scrub_header_string' ),
575             ###InternalReporTCSV message =>[ "The updated string is: $string" ], } );
576 29         15 $string =~ s/\r/ /gsxm;
577             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
578             ###InternalReporTCSV name_space => $self->get_all_space( '_scrub_header_string' ),
579             ###InternalReporTCSV message =>[ "The updated string is: $string" ], } );
580 29         29 $string =~ s/\s/_/gsxm;
581             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 0,
582             ###InternalReporTCSV name_space => $self->get_all_space( '_scrub_header_string' ),
583             ###InternalReporTCSV message =>[ "The updated string is: $string" ], } );
584 29         26 chomp $string;
585             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
586             ###InternalReporTCSV name_space => $self->get_all_space( '_scrub_header_string' ),
587             ###InternalReporTCSV message =>[ "The final string is: $string" ], } );
588 29         33 return $string;
589             }
590              
591             sub DEMOLISH{
592 5     5 0 1496 my ( $self ) = @_;
593             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 2,
594             ###InternalReporTCSV name_space => $self->get_all_space( 'DEMOLISH' ),
595             ###InternalReporTCSV message =>[ "Arrived at DEMOLISH" ], } ) if $switchboard;
596 5 50       133 if( $self->_has_file_handle ){
597 5         107 flock( $self->_get_file_handle, LOCK_UN );
598 5 50       114 close( $self->_get_file_handle ) or confess "Couldn't close the file handle";
599 5         128 $self->_clear_file_handle;
600             ###InternalReporTCSV $switchboard->master_talk( { report => 'log_file', level => 1,
601             ###InternalReporTCSV name_space => $self->get_all_space( 'DEMOLISH' ),
602             ###InternalReporTCSV message =>[ "Arrived at DEMOLISH" ], } ) if $switchboard;
603             }
604             }
605              
606             #########1 Phinish 3#########4#########5#########6#########7#########8#########9
607              
608             __PACKAGE__->meta->make_immutable;
609              
610             1;
611             # The preceding line will help the module return a true value
612              
613             #########1 main pod docs 3#########4#########5#########6#########7#########8#########9
614              
615             __END__
616              
617             =head1 NAME
618              
619             Log::Shiras::Report::CSVFile - A report base for csv files
620              
621             =head1 SYNOPSIS
622              
623             use Modern::Perl;
624             #~ use Log::Shiras::Unhide qw( :InternalReporTCSV );
625             use Log::Shiras::Switchboard;
626             use Log::Shiras::Telephone;
627             use Log::Shiras::Report;
628             use Log::Shiras::Report::CSVFile;
629             use Log::Shiras::Report::Stdout;
630             $ENV{hide_warn} = 1;
631             $| = 1;
632             my $operator = Log::Shiras::Switchboard->get_operator(
633             name_space_bounds =>{
634             UNBLOCK =>{
635             to_file => 'info',# for info and more urgent messages
636             },
637             },
638             reports =>{
639             to_file =>[{
640             superclasses =>[ 'Log::Shiras::Report::CSVFile' ],
641             roles =>[ 'Log::Shiras::Report' ],# checks inputs and class requirements
642             file => 'test.csv',
643             }],
644             }
645             );
646             my $telephone = Log::Shiras::Telephone->new( report => 'to_file' );
647             $telephone->talk( level => 'info', message => 'A new line' );
648             $telephone->talk( level => 'trace', message => 'A second line' );
649             $telephone->talk( level => 'warn', message =>[ {
650             header_0 => 'A third line',
651             new_header => 'new header starts here' } ] );
652            
653             #######################################################################################
654             # Synopsis file (test.csv) output
655             # 01: header_0
656             # 02: "A new line"
657             # 03: "A third line"
658             #######################################################################################
659            
660             #######################################################################################
661             # Synopsis file (test.csv) output with line 24 commented out
662             # 01: header_0,new_header
663             # 02: "A third line","new header starts here"
664             #######################################################################################
665            
666             =head1 DESCRIPTION
667              
668             This is a report module that can act as a destination in the
669             L<Log::Shiras::Switchboard/reports> name-space. It is meant to be fairly flexible and
670             will have most of the needed elements in the class without added roles. An instance
671             of the class can be built either with ->new or using the implied
672             L<MooseX::ShortCut::BuildInstance> helpers. (See lines 18 - 20 in the example) When the
673             report is set up any call to that report namespace will then implement the L<add_line
674             |/add_line> method of this class.
675              
676             As implied in the Synopsis one of the features of this class is the fact that it will try to
677             reconcile the headers to inbound data and header requests. This class will attempt to
678             reconcile any deviation between the first passed row and the header. Subsequent added
679             rows using a passed array ref will add all values without warning whether the count matches
680             the header count or not. Subsequent added rows using a passed hashref will only used the
681             headers in the fixed L<header|/header> list but will warn for any passed headers not matching
682             the header list.
683              
684             This class will attempt to obtain an exclusive lock on the file. If the file is previously
685             locked it will wait. That will allow you to attach more than one report script to the same
686             file name and not overwrite lines. On the other hand this does have the potential to create
687             scripts that appear to be hung.
688              
689             =head2 Warning
690              
691             This class will always use the header list when adding new hash values. As a consequence
692             there can be no duplicates in the header list after it is coereced to this files requirements.
693             Since the class allows for mixed passing of array refs and hash refs it also has the
694             no duplicate header requirement with array ref handling too.
695              
696             =head2 Attributes
697              
698             Data passed to ->new when creating an instance. For modification of these attributes
699             after the instance is created see the attribute methods.
700              
701             =head3 file
702              
703             =over
704              
705             B<Definition:> This is the file name to be used by the .csv file. This should include the
706             full file path. If the file does not exist then the file will be created.
707              
708             B<Default:> None
709              
710             B<Required:> Yes
711              
712             B<Range:> it must have a .csv extention and can be opened
713              
714             B<attribute methods>
715              
716             =over
717              
718             B<set_file_name( $file_name )>
719              
720             =over
721              
722             B<Description> used to set the attribute
723              
724             =back
725              
726             B<get_file_name>
727              
728             =over
729              
730             B<Description> used to return the current attribute value
731              
732             =back
733              
734             =back
735              
736             =back
737              
738             =head3 headers
739              
740             =over
741              
742             B<Definition:> This an array ref of the requested headers in the file. Each of the headers
743             must match header string requirements. The header strings will be coerced as needed buy forcing
744             then lower case and removing any newlines.
745              
746             B<Default:> None
747              
748             B<Required:> No
749              
750             B<Range:> An array ref of strings starting with a lower case letter and containing letters,
751             underscores, and numbers
752              
753             B<attribute methods>
754              
755             =over
756              
757             B<set_headers( $array_ref )>
758              
759             =over
760              
761             B<Description> used to set all the attribute at once
762              
763             =back
764              
765             B<get_headers>
766              
767             =over
768              
769             B<Description> used to return all the attribute at once
770              
771             =back
772              
773             B<has_headers>
774              
775             =over
776              
777             B<Description> predicate for the whole attribute
778              
779             =back
780              
781             B<number_of_headers>
782              
783             =over
784              
785             B<Description> Returns the complete header count list
786              
787             =back
788              
789             =back
790              
791             =back
792              
793             =head3 reconcile_headers
794              
795             =over
796              
797             B<Definition:> It may be that when you open a file the file already has headers. This
798             attribute determines if the action or L<requested headers|/headers> are merged with the
799             file headers. In the merge the file headers are given order precedence so new requested
800             headers wind up at the end even when that means the requested headers are added out of
801             order to the original request!
802              
803             B<Default:> 1 = the headers will be reconciled
804              
805             B<Range:> Boolean
806              
807             B<attribute methods>
808              
809             =over
810              
811             B<set_reconcile_headers( $bool )>
812              
813             =over
814              
815             B<Description> used to set the attribute
816              
817             =back
818              
819             B<should_reconcile_headers>
820              
821             =over
822              
823             B<Description> used to return the current attribute value
824              
825             =back
826              
827             =back
828              
829             =back
830              
831             =head3 test_first_row
832              
833             =over
834              
835             B<Definition:> It may be that when you send the first row after instance instantiation
836             that the row and the headers don't agree. This will update the requested headers (
837             L<and maybe the file headers|/reconcile headers>) with any variation between the two.
838             In the case of a passed array ref no header change is implemented but a warning is
839             emitted when the passed list and the header list don't have the same count. For
840             passed hash refs new headers are added to the end of the requested headers. After
841             the first line no warning is emitted for passed array refs that don't match and
842             new hash keys (and their values) that don't match the header will just be left off
843             the report. New hash keys for the first row will be added in a random order.
844              
845             B<Default:> 1 = the first row will attempt reconciliation
846              
847             B<Range:> Boolean
848              
849             B<attribute methods>
850              
851             =over
852              
853             B<should_test_first_row>
854              
855             =over
856              
857             B<Description> used to return the current attribute value
858              
859             =back
860              
861             =back
862              
863             =back
864              
865             =head2 Methods
866              
867             =head3 new( %args )
868              
869             =over
870              
871             B<Definition:> This creates a new instance of the CSVFile L<report
872             |Log::Shiras::Switchboard/reports> class.
873              
874             B<Range:> It will accept any or none of the L<Attributes|/Attributes>
875              
876             B<Returns:> A report class to be stored in the switchboard.
877              
878             =back
879              
880             =head3 add_line( $message_ref )
881              
882             =over
883              
884             B<Definition:> This is the method called by the switchboard to add lines to the report. It will
885             expect a message compatible with L<Log::Shiras::Switchboard/master_talk( $args_ref )>. There is
886             some flexibility in the consumption of the value within the 'message' key. This package will
887             check if there is more than one item and handle it like an elements list. If there is only one
888             item and it is a hash ref it will attempt to consume the hashref as having keys matching the
889             columns. Other single elements will be consumed as sub-elements of an element list.
890              
891             B<Returns:> 1 (or dies)
892              
893             =back
894              
895             =head1 GLOBAL VARIABLES
896              
897             =over
898              
899             =item B<$ENV{hide_warn}>
900              
901             The module will warn when debug lines are 'Unhide'n. In the case where the you
902             don't want these notifications set this environmental variable to true.
903              
904             =back
905              
906             =head1 SUPPORT
907              
908             =over
909              
910             L<Log-Shiras/issues|https://github.com/jandrew/Log-Shiras/issues>
911              
912             =back
913              
914             =head1 TODO
915              
916             =over
917              
918             B<1.> Nothing L<currently|/SUPPORT>
919              
920             =back
921              
922             =head1 AUTHOR
923              
924             =over
925              
926             =item Jed Lund
927              
928             =item jandrew@cpan.org
929              
930             =back
931              
932             =head1 COPYRIGHT
933              
934             This program is free software; you can redistribute
935             it and/or modify it under the same terms as Perl itself.
936              
937             The full text of the license can be found in the
938             LICENSE file included with this module.
939              
940             =head1 DEPENDENCIES
941              
942             =over
943              
944             L<perl 5.010|perl/5.10.0>
945              
946             L<utf8>
947              
948             L<version>
949              
950             L<Moose>
951              
952             L<MooseX::StrictConstructor>
953              
954             L<MooseX::HasDefaults::RO>
955              
956             L<MooseX::Types::Moose>
957              
958             L<Text::CSV_XS>
959              
960             L<File::Copy> - copy
961              
962             L<File::Temp>
963              
964             L<Carp> - confess cluck
965              
966             L<Fcntl> - :flock LOCK_EX LOCK_UN SEEK_END
967              
968             =back
969              
970             =cut
971              
972             #########1#########2 main pod documentation end 5#########6#########7#########8#########9