File Coverage

blib/lib/App/RecordStream/Operation/xform.pm
Criterion Covered Total %
statement 130 144 90.2
branch 26 38 68.4
condition 7 10 70.0
subroutine 17 19 89.4
pod 0 12 0.0
total 180 223 80.7


line stmt bran cond sub pod time code
1             package App::RecordStream::Operation::xform;
2              
3             our $VERSION = "4.0.25";
4              
5 6     6   52346 use strict;
  6         13  
  6         151  
6              
7 6     6   27 use base qw(App::RecordStream::Operation);
  6         6  
  6         698  
8              
9 6     6   1750 use App::RecordStream::Executor::Getopt;
  6         17  
  6         144  
10 6     6   29 use App::RecordStream::Executor;
  6         12  
  6         6248  
11              
12             sub init {
13 25     25 0 29 my $this = shift;
14 25         29 my $args = shift;
15              
16 25         92 my $executor_options = App::RecordStream::Executor::Getopt->new();
17 25         30 my $before = 0;
18 25         31 my $after = 0;
19 25         34 my $post_snippet;
20             my $pre_snippet;
21              
22             my $spec = {
23             'B|before=n' => \$before,
24             'A|after=n' => \$after,
25 1     1   728 'C|context=n' => sub { $before = $_[1]; $after = $_[1]; },
  1         2  
26 25         106 'post-snippet=s' => \$post_snippet,
27             'pre-snippet=s' => \$pre_snippet,
28             $executor_options->arguments(),
29             };
30              
31 25         99 $this->parse_options($args, $spec, ['bundling']);
32              
33 25         74 my $expression = $executor_options->get_string($args);
34 25         61 my $executor = $this->create_executor($expression, $post_snippet, $pre_snippet);
35              
36 25         38 $this->{'BEFORE'} = $before;
37 25         37 $this->{'AFTER'} = $after;
38 25         40 $this->{'EXECUTOR'} = $executor;
39              
40 25         50 $this->{'XFORM_REF'} = $executor->get_code_ref('xform');
41              
42 25         63 $this->{'BEFORE_ARRAY'} = [];
43 25         38 $this->{'AFTER_ARRAY'} = [];
44 25         31 $this->{'SPOOLED_INPUT'} = [];
45 25         34 $this->{'SPOOLED_OUTPUT'} = [];
46              
47 25         58 $executor->execute_method('pre_xform');
48 25         58 $this->handle_spools();
49             }
50              
51             sub create_executor {
52 25     25 0 33 my $this = shift;
53 25         28 my $snippet = shift;
54 25   100     76 my $post_snippet = shift || '';
55 25   100     60 my $pre_snippet = shift || '';
56              
57 25         141 my $args = {
58             xform => {
59             code => <<" CODE",
60             \$line++;
61             $snippet
62             ; # Safe from a trailing comment in \$snippet
63             \$r
64             CODE
65             arg_names => [qw(r filename B A)],
66             },
67             post_xform => {
68             code => $post_snippet,
69             },
70             pre_xform => {
71             code => $pre_snippet,
72             },
73             };
74              
75 25         36 my $executor;
76 25         28 eval {
77 25         106 $executor = App::RecordStream::Executor->new($args);
78             };
79              
80 25 50       49 if ( $@ ) {
81 0         0 die "FATAL: Problem compiling a snippet: $@";
82             }
83              
84             # Initialize the annonymous sub refs to contain $this
85             $executor->set_executor_method('push_input', sub {
86 3     3   28 $this->push_input(@_);
87 25         118 });
88              
89             $executor->set_executor_method('push_output', sub {
90 3     3   12 $this->push_output(@_);
91 25         85 });
92              
93 25         48 return $executor;
94             }
95              
96             sub push_input {
97 3     3 0 14 my $this = shift;
98 3         4 push @{$this->{'SPOOLED_INPUT'}}, @_;
  3         7  
99             }
100              
101             sub push_output {
102 3     3 0 4 my $this = shift;
103 3         5 $this->{'SUPPRESS_R'} = 1;
104 3         29 push @{$this->{'SPOOLED_OUTPUT'}}, @_;
  3         23  
105             }
106              
107             sub accept_record {
108 67     67 0 87 my $this = shift;
109 67         73 my $record = shift;
110              
111 67         89 my $before = $this->{'BEFORE'};
112 67         85 my $after = $this->{'AFTER'};
113              
114 67 100 100     206 if ( $before == 0 && $after == 0 ) {
115 58         115 return $this->run_record_with_context($record);
116             }
117              
118 9         11 my $before_array = $this->{'BEFORE_ARRAY'};
119 9         14 my $after_array = $this->{'AFTER_ARRAY'};
120 9         11 my $current_record = $this->{'CURRENT_RECORD'};
121              
122 9         12 push @$after_array, $record;
123              
124 9 100       15 if ( scalar @$after_array > $after ) {
125 7         8 my $new_record = shift @$after_array;
126              
127 7 100       14 unshift @$before_array, $current_record if ( $current_record );
128 7         11 $current_record = $new_record;
129             }
130              
131 9 50       13 if ( scalar @$after_array > $after ) {
132 0         0 my $new_record = shift @$after_array;
133              
134 0 0       0 pop @$before_array if ( scalar @$before_array > $before );
135 0 0       0 unshift @$before_array, $current_record if ( $current_record );
136 0         0 $current_record = $new_record;
137             }
138              
139 9         10 $this->{'CURRENT_RECORD'} = $current_record;
140 9 100       18 pop @$before_array if ( scalar @$before_array > $before );
141              
142 9 100       11 if ( !$current_record ) {
143 2         6 return 1;
144             }
145 7         8 $this->{'CURRENT_RECORD'} = $current_record;
146              
147 7         14 return $this->run_record_with_context($current_record, $before_array, $after_array);
148             }
149              
150             sub stream_done {
151 25     25 0 34 my $this = shift;
152              
153 25         38 my $after_array = $this->{'AFTER_ARRAY'};
154              
155 25 100       50 if ( scalar @$after_array > 0 ) {
156 2         3 my $before = $this->{'BEFORE'};
157 2         3 my $before_array = $this->{'BEFORE_ARRAY'};
158 2         3 my $current_record = $this->{'CURRENT_RECORD'};
159              
160 2         4 while ( scalar @$after_array ) {
161 2         4 my $new_record = shift @$after_array;
162 2 50       6 unshift @$before_array, $current_record if ( $current_record );
163 2         3 $current_record = $new_record;
164              
165 2 50       4 pop @$before_array if ( scalar @$before_array > $before );
166              
167 2         6 $this->run_record_with_context($current_record, $before_array, $after_array);
168             }
169             }
170              
171 25         78 $this->{'EXECUTOR'}->execute_method('post_xform');
172 25         58 $this->handle_spools();
173             }
174              
175             sub run_record_with_context {
176 67     67 0 83 my $this = shift;
177 67         74 my $record = shift;
178 67         102 my $before = shift;
179 67         78 my $after = shift;
180              
181 67         117 my $value = $this->run_xform_with_record($record, $before, $after);
182              
183 67 100       140 if ( ! $this->{'SUPPRESS_R'} ) {
184 66 100       145 if ( ref($value) eq 'ARRAY' ) {
185 4         6 foreach my $new_record (@$value) {
186 7 50       10 if ( ref($new_record) eq 'HASH' ) {
187 7         14 $this->push_record(App::RecordStream::Record->new($new_record));
188             }
189             else {
190 0         0 $this->push_record($new_record);
191             }
192             }
193             }
194             else {
195 62         149 $this->push_record($value);
196             }
197             }
198              
199 67         126 return $this->handle_spools();
200             }
201              
202             sub has_spooled_data {
203 0     0 0 0 my $this = shift;
204 0   0     0 return (scalar @{$this->{'SPOOLED_INPUT'}} > 0) || (scalar @{$this->{'SPOOLED_OUTPUT'}} > 0);
205             }
206              
207             sub handle_spools {
208 117     117 0 149 my $this = shift;
209              
210 117         160 $this->{'SUPPRESS_R'} = 0;
211              
212 117         127 while ( @{$this->{'SPOOLED_OUTPUT'}} ) {
  119         221  
213 2         3 my $new_record = shift @{$this->{'SPOOLED_OUTPUT'}};
  2         4  
214 2 50       5 if ( ref($new_record) eq 'HASH' ) {
215 2         6 $new_record = App::RecordStream::Record->new($new_record);
216             }
217              
218 2         5 $this->push_record($new_record);
219             }
220              
221 117         144 while ( @{$this->{'SPOOLED_INPUT'}} ) {
  120         197  
222 3         3 my $new_record = shift @{$this->{'SPOOLED_INPUT'}};
  3         5  
223 3 50       8 if ( ref($new_record) eq 'HASH' ) {
224 3         8 $new_record = App::RecordStream::Record->new($new_record);
225             }
226              
227 3 50       5 if (! $this->accept_record($new_record) ) {
228             #we've requested a stop, clear the input and return 0
229 0         0 $this->{'SPOOLED_INPUT'} = [];
230 0         0 return 0;
231             }
232             }
233              
234 117         760 return 1;
235             }
236              
237             sub run_xform_with_record {
238 67     67 0 75 my $this = shift;
239 67         73 my $record = shift;
240 67         81 my $before = shift;
241 67         70 my $after = shift;
242              
243 67 100       101 if ( $before ) {
244 9         13 $before = [@$before];
245 9         12 $after = [@$after];
246             }
247              
248             # Must copy before and after due to autovivification in the case of:
249             # {{after}} = $A->[0]->{'foo'}
250             # (which is unintintional vivification into the array in stream_done)
251 67         141 return $this->{'XFORM_REF'}->(
252             $record,
253             App::RecordStream::Operation::get_current_filename(),
254             $before,
255             $after,
256             );
257             }
258              
259             sub add_help_types {
260 25     25 0 38 my $this = shift;
261 25         65 $this->use_help_type('snippet');
262 25         39 $this->use_help_type('keyspecs');
263             }
264              
265             sub usage {
266 0     0 0   my $this = shift;
267              
268 0           my $options = [
269             App::RecordStream::Executor::options_help(),
270             ['A NUM', 'Make NUM records after this one available in $A (closest record to current in first position)'],
271             ['B NUM', 'Make NUM records before this one available in $B (closest record to current in first position)'],
272             ['C NUM', 'Make NUM records after this one available in $A and $B, as per -A NUM and -B NUM'],
273             ['post-snippet SNIP', 'A snippet to run once the stream has completed'],
274             ['pre-snippet SNIP', 'A snippet to run before the stream starts'],
275             ];
276              
277 0           my $args_string = $this->options_string($options);
278              
279 0           return <
280             Usage: recs-xform []
281             __FORMAT_TEXT__
282             is evaluated as perl on each record of input (or records from
283             ) with \$r set to a App::RecordStream::Record object and \$line set to the current
284             line number (starting at 1). All records are printed back out (changed as
285             they may be).
286              
287             If \$r is set to an ARRAY ref in the expr, then the values of the array will
288             be treated as records and outputed one to a line. The values of the array
289             may either be a hash ref or a App::RecordStream::Record object. The
290             original record will not be outputted in this case.
291              
292             There are two helper methods: push_input and push_output. Invoking
293             push_input on a Record object or hash will cause the next record to be
294             processed to be the indicated record. You may pass multiple records with
295             one call. Similarly push_output causes the next record to be output to be
296             the passed record(s). If push_output is called, the original record will
297             not be output in this case. (call push_output(\$r) if you want that record
298             also outputted). You may call these methods from a --pre-snippet or a
299             --post-snippet. You may also call push_output() without any argument to
300             suppress the outputting of the current record
301             __FORMAT_TEXT__
302              
303             $args_string
304              
305             Examples:
306             Add line number to records
307             recs-xform '\$r->{line} = \$line'
308             Rename field old to new, remove field a
309             recs-xform '\$r->rename("old", "new"); \$r->remove("a");'
310             Remove fields which are not "a", "b", or "c"
311             recs-xform '\$r->prune_to("a", "b", "c")'
312             Double records
313             recs-xform '\$r = [{\%\$r}, {\%\$r}]'
314             Double records with function interface
315             recs-xform 'push_output(\$r, \$r);'
316             Move a value from the previous record to the next record
317             recs-xform -B 1 '{{before_val}} = \$B->[0]'
318             USAGE
319             }
320              
321             1;