File Coverage

blib/lib/Catmandu/Fix/Parser.pm
Criterion Covered Total %
statement 194 195 99.4
branch 19 22 86.3
condition 12 16 75.0
subroutine 40 40 100.0
pod 1 22 4.5
total 266 295 90.1


line stmt bran cond sub pod time code
1             package Catmandu::Fix::Parser;
2              
3 154     154   1124 use Catmandu::Sane;
  154         388  
  154         2217  
4              
5             our $VERSION = '1.2020';
6              
7             use Catmandu::Util
8 154     154   1165 qw(check_value check_string is_array_ref is_instance is_able require_package);
  154         434  
  154         10963  
9 154     154   78204 use Module::Info;
  154         1040243  
  154         4684  
10 154     154   1076 use Moo;
  154         367  
  154         855  
11 154     154   55768 use namespace::clean;
  154         468  
  154         1083  
12              
13             extends 'Parser::MGC';
14              
15             has env => (is => 'lazy');
16             has default_ns => (is => 'lazy');
17              
18             sub FOREIGNBUILDARGS {
19 159     159 0 35734 my ($class, $opts) = @_;
20 159         481 $opts->{toplevel} = 'parse_statements';
21 159         1362 %$opts;
22             }
23              
24             sub _build_default_ns {
25 159     159   1571 my ($self) = @_;
26 159         518 $self->_build_ns('perl:catmandu.fix');
27             }
28              
29             sub _build_env {
30 159     159   1713 my ($self) = @_;
31 159         495 $self->init_env([]);
32             }
33              
34             sub init_env {
35 381     381 0 888 my ($self, $envs) = @_;
36 381         7928 splice(@$envs, 0, @$envs, {ns => {'' => $self->default_ns}});
37 381         37758 $envs;
38             }
39              
40             sub get_ns {
41 432     432 0 1031 my ($self, $name) = @_;
42 432         10216 my $envs = $self->env;
43 432         4014 for my $env (@$envs) {
44             return $env->{ns}{$name}
45 464 100 100     3278 if exists $env->{ns} && exists $env->{ns}{$name};
46             }
47 2         22 return;
48             }
49              
50             sub add_ns {
51 7     7 0 21 my ($self, $name, $ns) = @_;
52 7         238 my $env = $self->env->[-1];
53 7   50     91 ($env->{ns} //= {})->{$name} = $ns;
54             }
55              
56             sub scope {
57 401     401 0 908 my ($self, $block) = @_;
58 401         9046 my $envs = $self->env;
59 401         2521 push @$envs, +{};
60 401         903 my $res = $block->();
61              
62             # TODO ensure env gets popped after exception
63 394         27678 pop @$envs;
64 394         1028 $res;
65             }
66              
67             sub parse {
68 222     222 1 233646 my ($self, $source) = @_;
69              
70 222         1032 check_value($source);
71              
72             try {
73 222     222   29750 $self->from_string($source);
74             }
75             catch {
76 21     21   4047 my $err = $_;
77 21 100 66     184 if (ref($err) && ref($err) =~ /^Catmandu/) {
78 7 50       42 $err->set_source($source) if is_able($err, 'set_source');
79 7         28 $err->throw;
80             }
81 14         182 Catmandu::FixParseError->throw(message => $err, source => $source,);
82             }
83             finally {
84 222     222   12437 $self->init_env;
85 222         18766 };
86             }
87              
88             sub pattern_comment {
89 159     159 0 5581 qr/#[^\n]*/;
90             }
91              
92             sub parse_statements {
93 401     401 0 4607 my ($self) = @_;
94             my $statements
95 401     401   1987 = $self->scope(sub {$self->sequence_of('parse_statement')});
  401         1475  
96 394 100       1451 [grep defined, map {is_array_ref($_) ? @$_ : $_} @$statements];
  419         2731  
97             }
98              
99             sub parse_statement {
100 617     617 0 30642 my ($self) = @_;
101 617         1946 my $statement = $self->any_of(
102             'parse_block', 'parse_use', 'parse_filter', 'parse_if',
103             'parse_unless', 'parse_bind', 'parse_fix',
104             );
105              
106             # support deprecated separator
107 421         1564 $self->maybe_expect(';');
108 421         9350 $statement;
109             }
110              
111             sub parse_block {
112 617     617 0 9317 my ($self) = @_;
113 617         1949 $self->token_kw('block');
114 6         726 my $statements = $self->parse_statements;
115 6         22 $self->expect('end');
116 5         272 $statements;
117             }
118              
119             sub parse_use {
120 612     612 0 107224 my ($self) = @_;
121 612         1830 $self->token_kw('use');
122 7         544 my $args = $self->parse_arguments;
123 7         31 my $name = check_string(shift(@$args));
124 7         737 my $ns = $self->_build_ns($name);
125 7         200 my %opts = @$args;
126 7   66     50 $self->add_ns($opts{as} // $name, $ns);
127 7         45 return;
128             }
129              
130             sub parse_filter {
131 605     605 0 73547 my ($self) = @_;
132 605         1594 my $type = $self->token_kw('select', 'reject');
133 25         2020 my $name = $self->parse_name;
134 20         1561 my $args = $self->parse_arguments;
135              
136             # support deprecated separator
137 20         100 $self->maybe_expect(';');
138 20         628 $self->_build_condition(
139             $name, $args,
140             $type eq 'reject',
141             require_package('Catmandu::Fix::reject')->new
142             );
143             }
144              
145             sub parse_if {
146 585     585 0 68378 my ($self) = @_;
147 585         1662 $self->token_kw('if');
148 37         2791 my $name = $self->parse_name;
149 37         3079 my $args = $self->parse_arguments;
150              
151             # support deprecated separator
152 36         157 $self->maybe_expect(';');
153 36         1115 my $cond
154             = $self->_build_condition($name, $args, 1, $self->parse_statements);
155             my $elsif_conditions = $self->sequence_of(
156             sub {
157 38     38   2052 $self->token_kw('elsif');
158 4         415 my $name = $self->parse_name;
159 4         300 my $args = $self->parse_arguments;
160              
161             # support deprecated separator
162 4         18 $self->maybe_expect(';');
163 4         153 $self->_build_condition($name, $args, 1, $self->parse_statements);
164             }
165 36         289 );
166             my $else_fixes = $self->maybe(
167             sub {
168 36     36   632 $self->expect('else');
169 5         282 $self->parse_statements;
170             }
171 36         5884 );
172 36         2749 $self->expect('end');
173              
174             # support deprecated separator
175 34         1732 $self->maybe_expect(';');
176              
177 34         950 my $last_cond = $cond;
178              
179 34 50       161 if ($elsif_conditions) {
180 34         105 for my $c (@$elsif_conditions) {
181 4         18 $last_cond->fail_fixes([$c]);
182 4         10 $last_cond = $c;
183             }
184             }
185              
186 34 100       150 if ($else_fixes) {
187 5         19 $last_cond->fail_fixes($else_fixes);
188             }
189              
190 34         227 $cond;
191             }
192              
193             sub parse_unless {
194 551     551 0 62844 my ($self) = @_;
195 551         1614 $self->token_kw('unless');
196 13         993 my $name = $self->parse_name;
197 13         1078 my $args = $self->parse_arguments;
198              
199             # support deprecated separator
200 12         66 $self->maybe_expect(';');
201 12         382 my $cond
202             = $self->_build_condition($name, $args, 0, $self->parse_statements);
203 12         76 $self->expect('end');
204              
205             # support deprecated separator
206 11         771 $self->maybe_expect(';');
207 11         507 $cond;
208             }
209              
210             sub parse_bind {
211 540     540 0 62070 my ($self) = @_;
212 540         1437 my $type = $self->token_kw('bind', 'do', 'doset');
213 116         8921 my $name = $self->parse_name;
214 116         11290 my $args = $self->parse_arguments;
215              
216             # support deprecated separator
217 116         390 $self->maybe_expect(';');
218 116         3526 my $bind = $self->_build_bind($name, $args, $type eq 'doset',
219             $self->parse_statements);
220 116         467 $self->expect('end');
221              
222             # support deprecated separator
223 116         6577 $self->maybe_expect(';');
224 116         3884 $bind;
225             }
226              
227             sub parse_fix {
228 424     424 0 49112 my ($self) = @_;
229 424         1139 my $lft_name = $self->parse_name;
230 422         33019 my $lft_args = $self->parse_arguments;
231             my $bool = $self->maybe(
232             sub {
233             $self->any_of(
234 236         3774 sub {$self->expect(qr/and|&&/); 1},
  7         443  
235 229         22046 sub {$self->expect(qr/or|\|\|/); 0},
  3         178  
236 236     236   4110 );
237             }
238 236         1718 );
239              
240 236         24595 my $fix;
241              
242 236 100       2274 if (defined $bool) {
243 10         48 $self->commit;
244 10         96 my $rgt_name = $self->parse_name;
245 10         815 my $rgt_args = $self->parse_arguments;
246 9         40 $fix = $self->_build_condition($lft_name, $lft_args, $bool,
247             $self->_build_fix($rgt_name, $rgt_args));
248             }
249             else {
250 226         800 $fix = $self->_build_fix($lft_name, $lft_args);
251             }
252              
253             # support deprecated separator
254 228         12499 $self->maybe_expect(';');
255              
256 228         9656 $fix;
257             }
258              
259             sub parse_name {
260 629     629 0 1135 my ($self) = @_;
261 629         2526 $self->generic_token(
262             name => qr/(?:[a-z][_0-9a-zA-Z]*\.)*[a-z][_0-9a-zA-Z]*/);
263             }
264              
265             sub parse_arguments {
266 629     629 0 1416 my ($self) = @_;
267 629         2083 $self->expect('(');
268 444         23921 my $args = $self->list_of(qr/[,:]|=>/, 'parse_value');
269 444         46485 $self->expect(')');
270 440         21393 $args;
271             }
272              
273             sub parse_value {
274 745     745 0 58071 my ($self) = @_;
275 745         2201 $self->any_of('parse_double_quoted_string', 'parse_single_quoted_string',
276             'parse_bare_string',);
277             }
278              
279             sub parse_bare_string {
280 687     687 0 63089 my ($self) = @_;
281 687         2607 $self->generic_token(bare_string => qr/[^\s\\,;:=>()"']+/);
282             }
283              
284             sub parse_single_quoted_string {
285 740     740 0 69495 my ($self) = @_;
286              
287 740         2833 my $str = $self->generic_token(string => qr/'(?:\\?+.)*?'/);
288 53         3420 $str = substr($str, 1, length($str) - 2);
289              
290 53         145 $str =~ s{\\'}{'}gxms;
291              
292 53         347 $str;
293             }
294              
295             sub parse_double_quoted_string {
296 745     745 0 10588 my ($self) = @_;
297              
298 745         2788 my $str = $self->generic_token(string => qr/"(?:\\?+.)*?"/);
299 5         283 $str = substr($str, 1, length($str) - 2);
300              
301 5 100       30 if (index($str, '\\') != -1) {
302 3         8 $str =~ s/\\u([0-9A-Fa-f]{4})/chr(hex($1))/egxms;
  0         0  
303 3         7 $str =~ s/\\n/\n/gxms;
304 3         8 $str =~ s/\\r/\r/gxms;
305 3         6 $str =~ s/\\b/\b/gxms;
306 3         6 $str =~ s/\\f/\f/gxms;
307 3         5 $str =~ s/\\t/\t/gxms;
308 3         8 $str =~ s/\\\\/\\/gxms;
309 3         6 $str =~ s{\\/}{/}gxms;
310 3         8 $str =~ s{\\"}{"}gxms;
311             }
312              
313 5         34 $str;
314             }
315              
316             sub _build_condition {
317 81     81   11315 my ($self, $name, $args, $pass, $fixes) = @_;
318 81 100       423 $fixes = [$fixes] unless is_array_ref($fixes);
319 81         271 my $cond = $self->_build_fix($name, $args, 'Condition');
320 81 100       1949 if ($pass) {
321 56         312 $cond->pass_fixes($fixes);
322             }
323             else {
324 25         113 $cond->fail_fixes($fixes);
325             }
326 81         323 $cond;
327             }
328              
329             sub _build_bind {
330 116     116   399 my ($self, $name, $args, $return, $fixes) = @_;
331 116 50       428 $fixes = [$fixes] unless is_array_ref($fixes);
332 116         350 my $bind = $self->_build_fix($name, $args, 'Bind');
333 116         2777 $bind->__return__($return);
334 116         321 $bind->__fixes__($fixes);
335 116         269 $bind;
336             }
337              
338             sub _build_fix {
339 432     432   1098 my ($self, $name, $args, $type) = @_;
340 432         1391 my @name_parts = split(/\./, $name);
341 432         908 my $fix_name = pop @name_parts;
342 432         975 my $ns_name = join('.', @name_parts);
343 432   66     1120 my $ns = $self->get_ns($ns_name)
344             // Catmandu::FixParseError->throw("Unknown namespace: $ns_name");
345 430         1777 $ns->load($fix_name, $args, $type);
346             }
347              
348             sub _build_ns {
349 166     166   414 my ($self, $name) = @_;
350 166         665 my @name_parts = split(/:/, $name);
351 166         392 $name = pop @name_parts;
352 166   100     609 my $pkg_name = $name_parts[0] // 'perl';
353 166         647 my $pkg = require_package($pkg_name, 'Catmandu::Fix::Namespace');
354 166         2954 $pkg->new(name => $name);
355             }
356              
357             1;
358              
359             __END__
360              
361             =pod
362              
363             =head1 NAME
364              
365             Catmandu::Fix::Parser - the parser of the Catmandu::Fix language
366              
367             =head1 SYNOPSIS
368              
369             use Catmandu::Sane;
370             use Catmandu::Fix::Parser;
371             use Catmandu::Fix;
372              
373             use Data::Dumper;
374              
375             my $parser = Catmandu::Fix::Parser->new;
376              
377             my $fixes;
378              
379             try {
380             $fixes = $parser->parse(<<EOF);
381             add_field(test,123)
382             EOF
383             }
384             catch {
385             printf "[%s]\nscript:\n%s\nerror: %s\n"
386             , ref($_)
387             , $_->source
388             , $_->message;
389             };
390              
391             my $fixer = Catmandu::Fix->new(fixes => $fixes);
392              
393             print Dumper($fixer->fix({}));
394              
395             =head1 DESCRIPTION
396              
397             Programmers are discouraged to use the Catmandu::Parser directly in code but
398             use the Catmandu package that provides the same functionality:
399              
400             use Catmandu;
401              
402             my $fixer = Catmandu->fixer(<<EOF);
403             add_field(test,123)
404             EOF
405              
406             print Dumper($fixer->fix({}));
407              
408             =head1 METHODS
409              
410             =head2 new()
411              
412             Create a new Catmandu::Fix parser
413              
414             =head2 parse($string)
415              
416             Reads a string and returns a blessed object with parsed
417             Catmandu::Fixes. Throws an Catmandu::ParseError on failure.
418              
419             =head1 SEE ALSO
420              
421             L<Catmandu::Fix>
422              
423             Or consult the webpages below for more information on the Catmandu::Fix language
424              
425             http://librecat.org/Catmandu/#fixes
426             http://librecat.org/Catmandu/#fix-language
427              
428             =cut