File Coverage

blib/lib/Text/Parser/Rule.pm
Criterion Covered Total %
statement 147 147 100.0
branch 72 72 100.0
condition 15 15 100.0
subroutine 36 36 100.0
pod 3 4 75.0
total 273 274 99.6


line stmt bran cond sub pod time code
1 42     42   715476 use strict;
  42         129  
  42         1407  
2 42     42   294 use warnings;
  42         103  
  42         1907  
3              
4             package Text::Parser::Rule 1.000;
5              
6             # ABSTRACT: Makes it possible to write AWK-style parsing rules for Text::Parser
7              
8 42     42   1400 use Moose;
  42         998485  
  42         354  
9 42     42   293331 use MooseX::StrictConstructor;
  42         217894  
  42         679  
10 42     42   204942 use Text::Parser::Error;
  42         105  
  42         295  
11 42     42   25430 use Scalar::Util 'blessed', 'looks_like_number';
  42         448  
  42         2710  
12 42     42   6603 use String::Util ':all';
  42         37483  
  42         12449  
13 42         6610 use List::Util qw(reduce any all none notall first
14             max maxstr min minstr product sum sum0 pairs
15             unpairs pairkeys pairvalues pairfirst
16             pairgrep pairmap shuffle uniq uniqnum uniqstr
17 42     42   393 );
  42         103  
18 42     42   322 use Try::Tiny;
  42         107  
  42         115484  
19              
20              
21             sub BUILD {
22 76     76 0 209     my $self = shift;
23 76 100 100     2392     parser_exception("Rule created without required components")
24                     if not $self->_has_condition and not $self->_has_action;
25 74 100       2243     $self->action('return $0;') if not $self->_has_action;
26 74 100       2241     $self->_constr_condition if not $self->_has_condition;
27             }
28              
29             sub _constr_condition {
30 18     18   41     my $self = shift;
31 18         499     $self->condition(1);
32 18         500     $self->_has_blank_condition(1);
33             }
34              
35              
36             sub clone {
37 9     9 1 3311     my ( $self, %opt ) = ( shift, @_ );
38 9         35     return _clone_another_rule( \%opt, $self->_construct_from_rule(@_) );
39             }
40              
41             sub _construct_from_rule {
42 9     9   23     my ( $self, %opt ) = ( shift, @_ );
43 9         14     my %const = ();
44 9 100       33     $const{if} = $opt{if} if exists $opt{if};
45                 $const{if} = $self->condition
46                     if not( exists $const{if} )
47 9 100 100     224         and not( $self->_has_blank_condition );
48 9 100       26     $const{do} = $opt{do} if exists $opt{do};
49                 $const{do} = $self->action
50 9 100       209         if not( exists $const{do} );
51                 $const{dont_record}
52 9 100       213         = exists $opt{dont_record} ? $opt{dont_record} : $self->dont_record;
53                 $const{continue_to_next}
54                     = exists $opt{continue_to_next}
55                     ? $opt{continue_to_next}
56 9 100       227         : $self->continue_to_next;
57 9         33     return \%const;
58             }
59              
60             sub _clone_another_rule {
61 9     9   21     my ( $opt, $const ) = ( shift, shift );
62 9         253     my $r = Text::Parser::Rule->new($const);
63                 $r->add_precondition( $opt->{add_precondition} )
64 9 100       36         if exists $opt->{add_precondition};
65 9         36     return $r;
66             }
67              
68              
69             has condition => (
70                 is => 'rw',
71                 isa => 'Str',
72                 predicate => '_has_condition',
73                 init_arg => 'if',
74                 trigger => \&_set_condition,
75             );
76              
77             sub _set_condition {
78 74     74   159     my $self = shift;
79 74 100       2146     return if ( $self->condition =~ /^\s*$/ );
80 72         2162     $self->_has_blank_condition(0);
81 72         278     $self->_set_highest_nf;
82 72         1890     $self->_cond_sub_str( _gen_sub_str( $self->condition ) );
83 72         2002     $self->_cond_sub(
84                     _set_cond_sub( $self->condition, $self->_cond_sub_str ) );
85             }
86              
87             has _has_blank_condition => (
88                 is => 'rw',
89                 isa => 'Bool',
90                 lazy => 1,
91                 default => 1,
92             );
93              
94             sub _set_highest_nf {
95 157     157   280     my $self = shift;
96 157         415     my $nf = _get_min_req_fields( $self->_gen_joined_str );
97 157         5802     $self->_set_min_nf($nf);
98             }
99              
100             sub _gen_joined_str {
101 157     157   288     my $self = shift;
102 157         307     my (@strs) = ();
103 157 100       5310     push @strs, $self->condition if $self->_has_condition;
104 157 100       4860     push @strs, $self->action if $self->_has_action;
105 157 100       5583     push @strs, $self->_join_preconds('; ') if not $self->_no_preconds;
106 157         804     my $str = join '; ', @strs;
107             }
108              
109             sub _get_min_req_fields {
110 157     157   316     my $str = shift;
111                 my @indx
112 157         4008         = $str =~ /\$([0-9]+)|\$[{]([-][0-9]+)[}]|[$][{]([-]?[0-9]+)[+][}]/g;
113 157         479     my @inds = sort { $b <=> $a } ( grep { defined $_ } @indx );
  73         295  
  537         1228  
114 157 100       466     return 0 if not @inds;
115 113 100       646     ( $inds[0] >= -$inds[-1] ) ? $inds[0] : -$inds[-1];
116             }
117              
118             my $SUB_BEGIN = 'sub {
119             my ($self, $this) = (shift, shift);
120             my $__ = $this->_stashed_vars;
121             local $_ = $this->this_line;
122             ';
123              
124             my $SUB_END = '
125             }';
126              
127             sub _gen_sub_str {
128 157     157   298     my $str = shift;
129 157         385     my $anon = $SUB_BEGIN . _replace_awk_vars($str) . $SUB_END;
130 157         4623     return $anon;
131             }
132              
133             sub _replace_awk_vars {
134 157     157   300     local $_ = shift;
135 157         431     _replace_positional_indicators();
136 157         435     _replace_range_shortcut();
137 157 100       462     _replace_exawk_vars() if m/[~][a-z_][a-z0-9_]+/i;
138 157         655     return $_;
139             }
140              
141             sub _replace_positional_indicators {
142 157     157   394     s/\$0/\$this->this_line/g;
143 157         316     s/\$[{]([-][0-9]+)[}]/\$this->field($1)/g;
144 157         774     s/\$([0-9]+)/\$this->field($1-1)/g;
145             }
146              
147             sub _replace_range_shortcut {
148 157     157   304     s/\$[{]([-][0-9]+)[+][}]/\$this->join_range($1)/g;
149 157         291     s/\$[{]([0-9]+)[+][}]/\$this->join_range($1-1)/g;
150 157         283     s/\\\@[{]([-][0-9]+)[+][}]/[\$this->field_range($1)]/g;
151 157         280     s/\\\@[{]([0-9]+)[+][}]/[\$this->field_range($1-1)]/g;
152 157         311     s/\@[{]([-][0-9]+)[+][}]/\$this->field_range($1)/g;
153 157         274     s/\@[{]([0-9]+)[+][}]/\$this->field_range($1-1)/g;
154             }
155              
156             sub _replace_exawk_vars {
157 12     12   82     my (@varnames) = _uniq( $_ =~ /[~]([a-z_][a-z0-9_]+)/ig );
158 12         36     foreach my $var (@varnames) {
159 17         39         my $v = '~' . $var;
160 17         249         s/$v/\$__->{$var}/g;
161                 }
162             }
163              
164             sub _uniq {
165 12     12   31     my (%elem) = map { $_ => 1 } @_;
  18         70  
166 12         55     return ( keys %elem );
167             }
168              
169             has _cond_sub_str => (
170                 is => 'rw',
171                 isa => 'Str',
172                 init_arg => undef,
173             );
174              
175             sub _set_cond_sub {
176 157     157   382     my ( $rstr, $sub_str ) = @_;
177 157         20166     my $sub = eval $sub_str;
178 157 100       603     parser_exception("Bad rule syntax $rstr: $@: $sub_str")
179                     if not defined $sub;
180 156         4947     return $sub;
181             }
182              
183             has _cond_sub => (
184                 is => 'rw',
185                 isa => 'CodeRef',
186                 init_arg => undef,
187             );
188              
189             has min_nf => (
190                 is => 'ro',
191                 isa => 'Num',
192                 traits => ['Number'],
193                 init_arg => undef,
194                 default => 0,
195                 lazy => 1,
196                 handles => { _set_min_nf => 'set', }
197             );
198              
199              
200             has action => (
201                 is => 'rw',
202                 isa => 'Str',
203                 init_arg => 'do',
204                 predicate => '_has_action',
205                 trigger => \&_set_action,
206             );
207              
208             sub _set_action {
209 78     78   195     my $self = shift;
210 78         288     $self->_set_highest_nf;
211 78         2092     $self->_act_sub_str( _gen_sub_str( $self->action ) );
212 78         2161     $self->_act_sub( _set_cond_sub( $self->action, $self->_act_sub_str ) );
213             }
214              
215             has _act_sub => (
216                 is => 'rw',
217                 isa => 'CodeRef',
218                 init_arg => undef,
219             );
220              
221             has _act_sub_str => (
222                 is => 'rw',
223                 isa => 'Str',
224                 init_arg => undef,
225             );
226              
227              
228             has dont_record => (
229                 is => 'rw',
230                 isa => 'Bool',
231                 default => 0,
232                 lazy => 1,
233                 trigger => \&_check_continue_to_next,
234             );
235              
236             sub _check_continue_to_next {
237 43     43   86     my $self = shift;
238 43 100       1297     return if not $self->continue_to_next;
239 9 100       241     parser_exception(
240                     "Rule cannot continue to next if action result is recorded")
241                     if not $self->dont_record;
242             }
243              
244              
245             has continue_to_next => (
246                 is => 'rw',
247                 isa => 'Bool',
248                 default => 0,
249                 lazy => 1,
250                 trigger => \&_check_continue_to_next,
251             );
252              
253              
254             has _preconditions => (
255                 is => 'ro',
256                 isa => 'ArrayRef[Str]',
257                 init_arg => undef,
258                 default => sub { [] },
259                 lazy => 1,
260                 traits => ['Array'],
261                 handles => {
262                     _preconds => 'elements',
263                     add_precondition => 'push',
264                     _join_preconds => 'join',
265                     _get_precond => 'get',
266                     _no_preconds => 'is_empty',
267                 },
268             );
269              
270             after add_precondition => sub {
271                 my $self = shift;
272                 $self->_set_highest_nf;
273                 my $str = $self->_get_precond(-1);
274                 my $substr = _gen_sub_str($str);
275                 $self->_add_precond_substr($substr);
276                 $self->_add_precond_sub( _set_cond_sub( $str, $substr ) );
277             };
278              
279             has _precondition_substrs => (
280                 is => 'ro',
281                 isa => 'ArrayRef[Str]',
282                 init_arg => undef,
283                 default => sub { [] },
284                 lazy => 1,
285                 traits => ['Array'],
286                 handles => {
287                     _precond_substrs => 'elements',
288                     _add_precond_substr => 'push',
289                 }
290             );
291              
292             has _precond_subroutines => (
293                 is => 'ro',
294                 isa => 'ArrayRef[CodeRef]',
295                 init_arg => undef,
296                 default => sub { [] },
297                 lazy => 1,
298                 traits => ['Array'],
299                 handles => {
300                     _precond_subs => 'elements',
301                     _add_precond_sub => 'push',
302                 }
303             );
304              
305              
306             sub test {
307 34     34 1 4467     my $self = shift;
308 34 100       112     return 0 if not _check_parser_arg(@_);
309 31         61     my $parser = shift;
310 31 100       843     return 0 if not $parser->auto_split;
311 29         79     return $self->_test($parser);
312             }
313              
314             sub _check_parser_arg {
315 43 100   43   115     return 0 if not @_;
316 40         69     my $parser = shift;
317 40 100       275     return 0 if not defined blessed($parser);
318 39         209     $parser->isa('Text::Parser');
319             }
320              
321             sub _test {
322 659     659   1292     my ( $self, $parser ) = ( shift, shift );
323 659 100       18091     return 0 unless defined( $parser->this_line );
324 655 100       22797     return 0 if $parser->NF < $self->min_nf;
325 538 100 100     18556     return 0
326                     if not( $self->_no_preconds or $self->_test_preconditions($parser) );
327 532 100       14371     return 1 if $self->_has_blank_condition;
328 484         1273     return $self->_test_cond_sub($parser);
329             }
330              
331             sub _test_preconditions {
332 7     7   17     my ( $self, $parser ) = @_;
333 7         286     foreach my $cond ( $self->_precond_subs ) {
334 7         142         my $val = $cond->( $self, $parser );
335 7 100 100     70         return 0 if not defined $val or not $val;
336                 }
337 1         4     return 1;
338             }
339              
340             sub _test_cond_sub {
341 484     484   907     my ( $self, $parser ) = @_;
342 484         12260     my $cond = $self->_cond_sub;
343 484         8604     my $val = $cond->( $self, $parser );
344 484 100       2517     defined $val and $val;
345             }
346              
347              
348             sub run {
349 9     9 1 101     my $self = shift;
350 9 100       26     parser_exception("Method run on rule was called without a parser object")
351                     if not _check_parser_arg(@_);
352 8 100       379     return if not $_[0]->auto_split;
353 7 100       39     push @_, 1 if @_ < 2;
354 7         21     $self->_run(@_);
355             }
356              
357             sub _run {
358 187     187   377     my ( $self, $parser ) = ( shift, shift );
359 187 100       4894     return if nocontent( $self->action );
360 175         2780     my (@res) = $self->_call_act_sub( $parser, @_ );
361 175 100       36106     return if $self->dont_record;
362 96         3344     $parser->push_records(@res);
363             }
364              
365             sub _call_act_sub {
366 175     175   374     my ( $self, $parser, $test_line ) = @_;
367 175 100 100     592     return if $test_line and not defined $parser->this_line;
368 173         4470     my $act = $self->_act_sub;
369 173         3238     return ( $act->( $self, $parser ) );
370             }
371              
372             __PACKAGE__->meta->make_immutable;
373              
374 42     42   417 no Moose;
  42         101  
  42         339  
375              
376              
377             1;
378              
379             __END__
380            
381             =pod
382            
383             =encoding UTF-8
384            
385             =head1 NAME
386            
387             Text::Parser::Rule - Makes it possible to write AWK-style parsing rules for Text::Parser
388            
389             =head1 VERSION
390            
391             version 1.000
392            
393             =head1 SYNOPSIS
394            
395             use Text::Parser;
396            
397             my $parser = Text::Parser->new();
398             $parser->add_rule(
399             if => '$1 eq "NAME:"', # Some condition string
400             do => 'return $2;', # Some action to do when condition is met
401             dont_record => 1, # Directive to not record
402             continue_to_next => 1, # Directive to next rule till another rule
403             );
404             $parser->read(shift);
405            
406             =head1 DESCRIPTION
407            
408             This class is never used directly. Instead rules are created and managed in one of two ways:
409            
410             =over 4
411            
412             =item *
413            
414             via the C<L<add_rule|Text::Parser/"add_rule">> method of L<Text::Parser>
415            
416             =item *
417            
418             using C<L<applies_rule|Text::Parser::RuleSpec/"applies_rule">> function from L<Text::Parser::RuleSpec>
419            
420             =back
421            
422             In both cases, the arguments are the same.
423            
424             =head1 METHODS
425            
426             =head2 condition
427            
428             Read-write attribute accessor method. Returns a string with the condition string as supplied to the C<if> clause in the constructor.
429            
430             my $cond_str = $rule->condition();
431            
432             Or modify the condition of a given rule:
433            
434             $rule->condition($new_condition);
435            
436             =head2 action
437            
438             Read-write accessor method for the C<do> clause of the rule. This is similar to the C<condition> accessor method.
439            
440             my $act_str = $rule->action;
441             $rule->action($modified_action);
442            
443             =head2 dont_record
444            
445             Read-write boolean accessor method for the C<dont_record> attribute of the constructor.
446            
447             print "This rule will not record\n" if $rule->dont_record;
448            
449             =head2 continue_to_next
450            
451             Read-write boolean accessor method for the C<continue_to_next> attribute in the constructor.
452            
453             print "Continuing to the next rule\n" if $rule->continue_to_next;
454            
455             =head2 add_precondition
456            
457             Method that can be used to add more pre-conditions to a rule
458            
459             $rule->add_precondition('looks_like_number($1)');
460             # Check if the first field on line is a number
461            
462             When you call C<L<test|/"test">> on the rule, it tests all the pre-conditions and the regular condition. If any of them fail, the test returns a boolean false.
463            
464             This method is very useful when you clone a rule.
465            
466             =head2 test
467            
468             Method called internally in L<Text::Parser>. Runs code in C<if> block.
469            
470             print "I will run the task of the rule\n" if $rule->test;
471            
472             =head2 run
473            
474             Method called internally in L<Text::Parser>. Runs code in C<do> block, and saves the result as a record depending on C<dont_record>.
475            
476             my $result = $rule->run();
477            
478             =head1 CONSTRUCTOR
479            
480             =head2 new
481            
482             Instances of this class can be created using the C<new> constructor, but normally a user would never create a rule themselves:
483            
484             my $rule = Text::Parser::Rule->new(
485             if => '# some condition string',
486             do => '# some task rule',
487             # At least one of the above two clauses must be specified
488             # When not specified, if clause defaults to 1
489             # When not specified, do clause defaults to 'return $_;'
490             dont_record => 1, # default: 0
491             continue_to_next => 1, # default: 0
492             );
493            
494             =head2 clone
495            
496             You can clone a rule and construct another rule from it.
497            
498             $new_rule = $rule->clone();
499             # Just creates a clone of $rule.
500            
501             The above is not particularly useful as it just creates a copy of the same rule. In the below example, we demonstrate that any of the four main attributes of a rule could be changed while creating a clone. For example:
502            
503             $rule = Text::Parser::Rule->new(
504             if => '# some condition',
505             );
506             $new_rule = $rule->clone(
507             # all of these are optional
508             do => '# modify original action',
509             dont_record => 1,
510             continue_to_next => 1,
511             );
512            
513             You could also change the C<if> clause above, but you could also add a pre-condition at the time of creating C<$new_rule> without affecting C<$rule> itself:
514            
515             $new_rule = $rule->clone(
516             add_precondition => '# another condition',
517             # ...
518             );
519            
520             The C<clone> method is just another way to create a rule. It just uses an existing rule as a seed.
521            
522             =head1 SEE ALSO
523            
524             =over 4
525            
526             =item *
527            
528             L<Text::Parser>
529            
530             =item *
531            
532             L<"The AWK Programming Language"|https://books.google.com/books?id=53ueQgAACAAJ&dq=The+AWK+Programming+Language&hl=en&sa=X&ei=LXxXVfq0GMOSsAWrpoC4Bg&ved=0CCYQ6AEwAA> by Alfred V. Aho, Brian W. Kernighan, and Peter J. Weinberger, Addison-Wesley, 1988. ISBN 0-201-07981-X
533            
534             =back
535            
536             =head1 BUGS
537            
538             Please report any bugs or feature requests on the bugtracker website
539             L<http://github.com/balajirama/Text-Parser/issues>
540            
541             When submitting a bug or request, please include a test-file or a
542             patch to an existing test-file that illustrates the bug or desired
543             feature.
544            
545             =head1 AUTHOR
546            
547             Balaji Ramasubramanian <balajiram@cpan.org>
548            
549             =head1 COPYRIGHT AND LICENSE
550            
551             This software is copyright (c) 2018-2019 by Balaji Ramasubramanian.
552            
553             This is free software; you can redistribute it and/or modify it under
554             the same terms as the Perl 5 programming language system itself.
555            
556             =cut
557