File Coverage

blib/lib/Struct/Path/PerlStyle.pm
Criterion Covered Total %
statement 150 150 100.0
branch 74 74 100.0
condition 25 25 100.0
subroutine 15 15 100.0
pod 2 2 100.0
total 266 266 100.0


line stmt bran cond sub pod time code
1             package Struct::Path::PerlStyle;
2              
3 8     8   272654 use 5.010;
  8         79  
4 8     8   44 use strict;
  8         17  
  8         218  
5 8     8   50 use warnings FATAL => 'all';
  8         17  
  8         370  
6 8     8   3772 use parent 'Exporter';
  8         2248  
  8         40  
7 8     8   4470 use utf8;
  8         134  
  8         43  
8              
9 8     8   252 use Carp 'croak';
  8         20  
  8         364  
10 8     8   4145 use Safe;
  8         292833  
  8         521  
11 8     8   5485 use Text::Balanced qw(extract_bracketed extract_quotelike);
  8         136926  
  8         751  
12 8     8   66 use re qw(is_regexp regexp_pattern);
  8         17  
  8         3279  
13              
14             require Struct::Path::PerlStyle::Functions;
15              
16             our @EXPORT_OK = qw(
17             path2str
18             str2path
19             );
20              
21             =encoding utf8
22              
23             =head1 NAME
24              
25             Struct::Path::PerlStyle - Perl-style syntax frontend for L.
26              
27             =begin html
28              
29             Travis CI
30             Coverage Status
31             CPAN version
32              
33             =end html
34              
35             =head1 VERSION
36              
37             Version 0.93
38              
39             =cut
40              
41             our $VERSION = '0.93';
42              
43             =head1 SYNOPSIS
44              
45             use Struct::Path qw(path);
46             use Struct::Path::PerlStyle qw(path2str str2path);
47              
48             my $nested = {
49             a => {
50             b => ["B0", "B1", "B2"],
51             c => ["C0", "C1"],
52             d => {},
53             },
54             };
55              
56             my @found = path($nested, str2path('{a}{}[0,2]'), deref => 1, paths => 1);
57              
58             while (@found) {
59             my $path = shift @found;
60             my $data = shift @found;
61              
62             print "path '" . path2str($path) . "' refer to '$data'\n";
63             }
64              
65             # path '{a}{b}[0]' refer to 'B0'
66             # path '{a}{b}[2]' refer to 'B2'
67             # path '{a}{c}[0]' refer to 'C0'
68              
69             =head1 EXPORT
70              
71             Nothing is exported by default.
72              
73             =head1 PATH SYNTAX
74              
75             Path is a sequence of 'steps', each represents nested level in the structure.
76              
77             =head2 Hashes
78              
79             Like in perl hash keys should be specified using curly brackets
80              
81             {} # all values from a's subhash
82             {foo} # value for 'foo' key
83             {foo,bar} # slicing: 'foo' and 'bar' values
84             {"space inside"} # key must be quoted unless it is a simple word
85             {"multi\nline"} # special characters interpolated when double quoted
86             {/pattern/mods} # keys regexp match
87              
88             =head2 Arrays
89              
90             Square brackets used for array indexes specification
91              
92             [] # all array items
93             [9] # 9-th element
94             [0,1,2,5] # slicing: 0, 1, 2 and 5 array items
95             [0..2,5] # same, but using ranges
96             [9..0] # descending ranges allowed
97              
98             =head2 Hooks
99              
100             Expressions enclosed in parenthesis treated as hooks and evaluated using
101             L compartment. Almost all perl operators and core functions available,
102             see L for more info. Some path related functions provided by
103             L.
104              
105             [](/pattern/mods) # match array values by regular expression
106             []{foo}(eq "bar" && BACK) # select hashes which have pair 'foo' => 'bar'
107              
108             There are two global variables available whithin safe compartment: C<$_> which
109             refers to value and C<%_> which provides current path via key C (in
110             L notation) and structure levels refs stack via key C.
111              
112             =head2 Aliases
113              
114             String in angle brackets is an alias - shortcut mapped into sequence of
115             steps. Aliases resolved iteratively, so alias may also refer into path with
116             another aliases.
117              
118             Aliases may be defined via global variable
119              
120             $Struct::Path::PerlStyle::ALIASES = {
121             foo => '{some}{long}{path}',
122             bar => '{and}{few}{steps}{more}'
123             };
124              
125             and then
126              
127             # expands to '{some}{long}{path}{and}{few}{steps}{more}'
128              
129             or as option for C:
130              
131             str2path('', {aliases => {foo => '{long}{path}'}});
132              
133             =head1 SUBROUTINES
134              
135             =cut
136              
137             our $ALIASES;
138              
139             my %ESCP = (
140             '"' => '\"',
141             "\a" => '\a',
142             "\b" => '\b',
143             "\t" => '\t',
144             "\n" => '\n',
145             "\f" => '\f',
146             "\r" => '\r',
147             "\e" => '\e',
148             );
149             my $ESCP = join('', sort keys %ESCP);
150              
151             my %INTP = map { $ESCP{$_} => $_ } keys %ESCP; # swap keys <-> values
152             my $INTP = join('|', map { "\Q$_\E" } sort keys %INTP);
153              
154             # $_ will be substituted (if omitted) as first arg if placed on start of
155             # hook expression
156             my $COMPL_OPS = join('|', (
157             '==',
158             '!=',
159             '=~',
160             '!~',
161             'eq',
162             'ne',
163             '<',
164             '>',
165             '<=',
166             '>=',
167             'lt',
168             'gt',
169             'le',
170             'ge',
171             '~~',
172             ));
173              
174             my $HASH_KEY_CHARS = qr/[\p{Alnum}_\.\-\+]/;
175              
176             our $HOOK_STRICT = 1;
177              
178             my $SAFE = Safe->new;
179             $SAFE->share_from(
180             'Struct::Path::PerlStyle::Functions',
181             \@Struct::Path::PerlStyle::Functions::EXPORT_OK
182             );
183             $SAFE->deny('warn');
184              
185             my $QR_MAP = {
186             '' => sub { qr/$_[0]/ },
187             i => sub { qr/$_[0]/i },
188             m => sub { qr/$_[0]/m },
189             s => sub { qr/$_[0]/s },
190             x => sub { qr/$_[0]/x },
191             im => sub { qr/$_[0]/im },
192             is => sub { qr/$_[0]/is },
193             ix => sub { qr/$_[0]/ix },
194             ms => sub { qr/$_[0]/ms },
195             mx => sub { qr/$_[0]/mx },
196             sx => sub { qr/$_[0]/sx },
197             ims => sub { qr/$_[0]/ims },
198             imx => sub { qr/$_[0]/imx },
199             isx => sub { qr/$_[0]/isx },
200             msx => sub { qr/$_[0]/msx },
201             imsx => sub { qr/$_[0]/imsx },
202             };
203              
204             =head2 str2path
205              
206             Convert perl-style string to L path structure
207              
208             $struct = str2path($string);
209              
210             =cut
211              
212             sub _push_hash {
213 75     75   163 my ($steps, $text) = @_;
214 75         126 my ($body, $delim, $mods, %step, $token, $type);
215              
216             # extract_quotelike fails to parse bare zero as a string
217 75 100       163 push @{$step{K}}, $text if $text eq '0';
  1         3  
218              
219 75         154 while ($text) {
220 142         379 ($token, $text, $type, $delim, $body, $mods) =
221             (extract_quotelike($text))[0,1,3,4,5,10];
222              
223 142 100 100     10836 if (not defined $delim) { # bareword
    100 100        
    100 100        
    100 100        
224 57 100       406 push @{$step{K}}, $token = $1
  55         698  
225             if ($text =~ s/^\s*($HASH_KEY_CHARS+)//);
226             } elsif (!$type and $delim eq '"') {
227 39         197 $body =~ s/($INTP)/$INTP{$1}/gs; # interpolate
228 39         65 push @{$step{K}}, $body;
  39         105  
229             } elsif (!$type and $delim eq "'") {
230 11         15 push @{$step{K}}, $body;
  11         29  
231             } elsif ($delim eq '/' and !$type or $type eq 'm') {
232 33         119 $mods = join('', sort(split('', $mods)));
233 33         59 eval { push @{$step{K}}, $QR_MAP->{$mods}->($body) };
  33         49  
  33         130  
234 33 100       93 if ($@) {
235 3         18 (my $err = $@) =~ s/ at .+//s;
236 3         8 croak "Step #" . scalar @{$steps} . " $err";
  3         335  
237             }
238             } else { # things like qr, qw and so on
239 2         6 substr($text, 0, 0, $token);
240 2         5 undef $token;
241             }
242              
243 139 100       708 croak "Unsupported key '$text', step #" . @{$steps}
  4         545  
244             if (!defined $token);
245              
246 135         271 $text =~ s/^\s+//; # discard trailing spaces
247              
248 135 100       308 if ($text ne '') {
249 78 100       266 if ($text =~ s/^,//) {
250 71 100       197 croak "Trailing delimiter at step #" . @{$steps}
  2         217  
251             if ($text eq '');
252             } else {
253 7         17 croak "Delimiter expected before '$text', step #" . @{$steps};
  7         775  
254             }
255             }
256             }
257              
258 59         99 push @{$steps}, \%step;
  59         213  
259             }
260              
261             sub _push_hook {
262 31     31   71 my ($steps, $text) = @_;
263              
264             # substitute default value if omitted
265 31         287 $text =~ s/^\s*($COMPL_OPS)/\$_ $1/;
266              
267 31         105 my $hook = 'sub {' .
268             '$^W = 0; ' .
269             'local %_ = ("path", $_[0], "refs", $_[1]); ' .
270             'local $_ = (ref $_[1] eq "ARRAY" and @{$_[1]}) ? ${$_[1]->[-1]} : undef; ' .
271             $text .
272             '}';
273              
274 31     2   370 open (local *STDERR,'>', \(my $stderr)); # catch compilation errors
  2         14  
  2         4  
  2         17  
275              
276 31 100       1637 unless ($hook = $SAFE->reval($hook, $HOOK_STRICT)) {
277 10 100       6365 if ($stderr) {
278 1         7 $stderr =~ s/ at \(eval \d+\) .+//s;
279 1         4 $stderr = " ($stderr)";
280             } else {
281 9         19 $stderr = "";
282             }
283              
284 10         31 (my $err = $@) =~ s/ at \(eval \d+\) .+//s;
285 10         26 croak "Failed to eval hook '$text': $err, step #" . @{$steps} . $stderr;
  10         1359  
286             }
287              
288 21         14207 push @{$steps}, $hook;
  21         195  
289             }
290              
291             sub _push_list {
292 57     57   124 my ($steps, $text) = @_;
293 57         91 my (@range, @step);
294              
295 57         215 for my $i (split /\s*,\s*/, $text, -1) {
296             @range = grep {
297 69 100       211 croak "Incorrect array index '$i', step #" . @{$steps}
  7         837  
298 80 100       145 unless (eval { $_ == int($_) });
  80         411  
299             } ($i =~ /^\s*(-?\d+)\s*\.\.\s*(-?\d+)\s*$/) ? ($1, $2) : $i;
300              
301 62 100       231 push @step, $range[0] < $range[-1]
302             ? $range[0] .. $range[-1]
303             : reverse $range[-1] .. $range[0];
304             }
305              
306 50         90 push @{$steps}, \@step;
  50         171  
307             }
308              
309             sub str2path($;$) {
310 116     116 1 69976 my ($path, $opts) = @_;
311              
312 116 100       485 croak "Undefined path passed" unless (defined $path);
313              
314 115 100       312 local $ALIASES = $opts->{aliases} if (exists $opts->{aliases});
315              
316 115         201 my (@steps, $step, $type);
317              
318 115         246 while ($path) {
319             # separated match: to be able to have another brackets inside;
320             # currently mostly for hooks, for example: '( $x > $y )'
321 182         370 for ('{"}', '["]', '(")', '<">') {
322 358         950 ($step, $path) = extract_bracketed($path, $_, '');
323 358 100       39361 last if ($step);
324             }
325              
326 182 100       1381 croak "Unsupported thing in the path, step #" . @steps . ": '$path'"
327             unless ($step);
328              
329 173         398 $type = substr $step, 0, 1, ''; # remove leading bracket
330 173         317 substr $step, -1, 1, ''; # remove trailing bracket
331              
332 173 100       472 if ($type eq '{') {
    100          
    100          
333 75         196 _push_hash(\@steps, $step);
334             } elsif ($type eq '[') {
335 57         145 _push_list(\@steps, $step);
336             } elsif ($type eq '(') {
337 31         80 _push_hook(\@steps, $step);
338             } else { # <>
339 10 100       315 croak "Unknown alias '$step'" unless (exists $ALIASES->{$step});
340              
341 8         18 substr $path, 0, 0, $ALIASES->{$step};
342 8         11 redo;
343             }
344             }
345              
346 71         399 return \@steps;
347             }
348              
349             =head2 path2str
350              
351             Convert L path structure to perl-style string
352              
353             $string = path2str($struct);
354              
355             =cut
356              
357             sub path2str($) {
358 47     47 1 35393 my $path = shift;
359              
360 47 100       266 croak "Arrayref expected for path" unless (ref $path eq 'ARRAY');
361 46         79 my $out = '';
362 46         85 my $sc = 0; # step counter
363              
364 46         69 for my $step (@{$path}) {
  46         115  
365 67         103 my @items;
366              
367 67 100       183 if (ref $step eq 'ARRAY') {
    100          
368 27         41 for my $i (@{$step}) {
  27         74  
369             croak "Incorrect array index '" . ($i // 'undef') . "', step #$sc"
370 65 100 100     95 unless (eval { int($i) == $i });
  65         480  
371 62 100 100     279 if (@items and (
      100        
372             $items[-1][0] < $i and $items[-1][-1] == $i - 1 or # ascending
373             $items[-1][0] > $i and $items[-1][-1] == $i + 1 # descending
374             )) {
375 30         59 $items[-1][1] = $i; # update range
376             } else {
377 32         66 push @items, [$i]; # new range
378             }
379             }
380              
381 24         44 for (@{items}) {
382             $_ = abs($_->[0] - $_->[-1]) < 2
383 32 100       85 ? join(',', @{$_})
  21         60  
384             : "$_->[0]..$_->[-1]"
385             }
386              
387 24         58 $out .= "[" . join(",", @{items}) . "]";
388             } elsif (ref $step eq 'HASH') {
389 39         53 my $keys;
390              
391 39 100       82 if (exists $step->{K}) {
    100          
392             croak "Unsupported hash keys definition, step #$sc"
393 36 100       193 unless (ref $step->{K} eq 'ARRAY');
394             croak "Unsupported hash definition (extra keys), step #$sc"
395 35 100       52 if (keys %{$step} > 1);
  35         189  
396 34         59 $keys = $step->{K};
397 3         11 } elsif (keys %{$step}) {
398 1         189 croak "Unsupported hash definition (unknown keys), step #$sc";
399             } else {
400 2         4 $keys = [];
401             }
402              
403 36         59 for my $k (@{$keys}) {
  36         64  
404 84 100 100     734 if (is_regexp($k)) {
    100          
405 15         50 my ($patt, $mods) = regexp_pattern($k);
406 15         35 $mods =~ s/[dlu]//g; # for Perl's internal use (c) perlre
407 15         46 push @items, "/$patt/$mods";
408              
409             } elsif (defined $k and ref $k eq '') {
410 67         111 push @items, $k;
411              
412 67 100       400 unless ($k =~ /^$HASH_KEY_CHARS+$/) {
413 33         141 $items[-1] =~ s/([\Q$ESCP\E])/$ESCP{$1}/gs; # escape
414 33         92 $items[-1] = qq("$items[-1]"); # quote
415             }
416             } else {
417 2   100     210 croak "Unsupported hash key type '" .
418             (ref($k) || 'undef') . "', step #$sc"
419             }
420             }
421              
422 34         126 $out .= "{" . join(",", @items) . "}";
423             } else {
424 1         101 croak "Unsupported thing in the path, step #$sc";
425             }
426 58         123 $sc++;
427             }
428              
429 37         101 return $out;
430             }
431              
432             =head1 AUTHOR
433              
434             Michael Samoglyadov, C<< >>
435              
436             =head1 BUGS
437              
438             Please report any bugs or feature requests to
439             C, or through the web interface at
440             L. I
441             will be notified, and then you'll automatically be notified of progress on
442             your bug as I make changes.
443              
444             =head1 SUPPORT
445              
446             You can find documentation for this module with the perldoc command.
447              
448             perldoc Struct::Path::PerlStyle
449              
450             You can also look for information at:
451              
452             =over 4
453              
454             =item * RT: CPAN's request tracker (report bugs here)
455              
456             L
457              
458             =item * AnnoCPAN: Annotated CPAN documentation
459              
460             L
461              
462             =item * CPAN Ratings
463              
464             L
465              
466             =item * Search CPAN
467              
468             L
469              
470             =back
471              
472             =head1 SEE ALSO
473              
474             L, L, L
475             L, L, L
476              
477             =head1 LICENSE AND COPYRIGHT
478              
479             Copyright 2016-2019 Michael Samoglyadov.
480              
481             This program is free software; you can redistribute it and/or modify it
482             under the terms of either: the GNU General Public License as published
483             by the Free Software Foundation; or the Artistic License.
484              
485             See L for more information.
486              
487             =cut
488              
489             1; # End of Struct::Path::PerlStyle