File Coverage

blib/lib/Data/SPath.pm
Criterion Covered Total %
statement 107 125 85.6
branch 29 36 80.5
condition 1 7 14.2
subroutine 20 25 80.0
pod n/a
total 157 193 81.3


line stmt bran cond sub pod time code
1 3     3   62970 use strict;
  3         7  
  3         127  
2 3     3   16 use warnings;
  3         7  
  3         154  
3             package Data::SPath;
4             BEGIN {
5 3     3   86 $Data::SPath::VERSION = '0.0004';
6             }
7             #ABSTRACT: lookup on nested data with simple path notation
8              
9 3     3   77 use 5.010_000;
  3         9  
  3         136  
10 3     3   16 use feature qw(switch);
  3         6  
  3         419  
11 3     3   17 use Carp qw/croak/;
  3         5  
  3         247  
12 3     3   24 use Scalar::Util qw/reftype blessed/;
  3         6  
  3         367  
13 3         479 use Text::Balanced qw/
14             extract_delimited
15             extract_bracketed
16             extract_multiple
17 3     3   3939 /;
  3         82854  
18              
19 3         523 use Sub::Exporter -setup => {
20             exports => [ spath => \&_build_spath ]
21 3     3   7539 };
  3         49366  
22              
23             my @Error_Handlers = qw(
24             method_miss
25             key_miss
26             index_miss
27             key_on_non_hash
28             args_on_non_method
29             );
30              
31              
32             sub _build_spath {
33 3     3   2012 my ( $class, $name, $args ) = @_;
34              
35             return sub {
36 109     109   79084 my ( $data, $path, $opts ) = @_;
37 109         242 for ( @Error_Handlers ) {
38 545 100       1498 unless ( exists $opts->{ $_ } ) {
39 540 100       1207 if ( exists $args->{ $_ } ) {
40 45         106 $opts->{ $_ } = $args->{ $_ };
41             }
42             else {
43 495         2092 $opts->{ $_ } = \&{ "_$_" };
  495         9686  
44             }
45             }
46 3     3   2880 no warnings 'uninitialized';
  3         7  
  3         4880  
47 545 50       1959 unless ( ref( $opts->{ $_ } ) eq 'CODE' ) {
48 0         0 croak "$_ must be set to a code reference";
49             }
50             }
51 109         465 return _spath( $data, $path, $opts );
52 3         47 };
53             }
54              
55             # taken from Data::DPath
56             sub _unescape {
57 179     179   600 my ( $str ) = @_;
58 179 50       472 return unless defined $str;
59 179         361 $str =~ s/(?
60 179         260 $str =~ s/\\{2}/\\/g;
61 179         2130 return $str;
62             }
63              
64             # Modified from Data::DPath. Added /s modifier to allow new lines in keys (why
65             # not?)
66             # this originally only supported double quote
67             sub _unquote {
68 41     41   213 my ($str) = @_;
69 41         213 $str =~ s/^(['"])(.*)\1$/$2/sg;
70 41         118 return $str;
71             }
72              
73 165     165   705 sub _quoted { shift =~ m,^/["'], }
74              
75             sub _method_miss {
76 0     0   0 my ( $method_name, $current, $depth ) = @_;
77 0         0 my $reftype = reftype( $current );
78 0         0 croak "tried to call nonexistent method '"
79             . $method_name
80             . "' on object with type $reftype at spath path element "
81             . $depth;
82             }
83              
84             sub _key_miss {
85 0     0   0 my ( $key, $current, $depth ) = @_;
86 0         0 croak "tried to access nonexistent key '"
87             . $key
88             . "' in hash at spath path element "
89             . $depth;
90             }
91              
92             sub _index_miss {
93 0     0   0 my ( $index, $current, $depth ) = @_;
94 0         0 croak "tried to access nonexistent index '"
95             . $index
96             . "' in array at spath path element "
97             . $depth;
98             }
99              
100             sub _key_on_non_hash {
101 0     0   0 my ( $key, $current, $depth ) = @_;
102 0   0     0 my $reftype = reftype( $current ) || '(non reference)';
103 0         0 croak "tried to access key '"
104             . $key
105             . "' on a non-hash type "
106             . $reftype
107             . " at spath path element "
108             . $depth;
109             }
110              
111             sub _args_on_non_method {
112 0     0   0 my ( $key, $current, $args, $depth ) = @_;
113 0   0     0 my $reftype = reftype( $current ) || '(non reference)';
114 0         0 croak "tried to pass arguments '"
115             . $args
116             . "' to a non-method '"
117             . $key
118             . "' of type "
119             . $reftype
120             . "at spath path element "
121             . $depth;
122             }
123              
124              
125              
126             sub _tokenize {
127 109     109   150 my ( $path ) = @_;
128              
129 109         148 my $remaining_path = $path;
130 109         126 my $extracted;
131             my @tokens;
132              
133 109         233 while ( $remaining_path ) {
134 165         213 my ( $prefix, $args );
135 0         0 my $key;
136              
137 165 100       353 if ( _quoted( $remaining_path ) ) {
138 31         86 ( $key, $remaining_path ) = extract_delimited( $remaining_path, q|'"|, '/' );
139 31         5075 ( $args, $remaining_path ) = extract_bracketed( $remaining_path, q|('")| );
140 31         1737 $key = _unescape _unquote $key;
141              
142             }
143             else {
144             # must extract arguments first to keep extract_delimited from getting
145             # quoted structures with / in them
146 134 100       653 if ( $remaining_path =~ m,^/[^/]+\(, ) {
147 7         31 ( $extracted, $remaining_path, $prefix ) = extract_bracketed( $remaining_path, q|('")|, '[^(]*' );
148 7 50 33     1874 if ( defined $prefix or defined $remaining_path ) {
149 3     3   26 no warnings 'uninitialized';
  3         8  
  3         5441  
150 7         16 $remaining_path = $prefix . $remaining_path;
151 7         14 $args = $extracted;
152             }
153             else {
154 0         0 $remaining_path = $extracted;
155             }
156             }
157 134         901 ( $extracted, $remaining_path ) = extract_delimited( $remaining_path, '/' );
158 134 100       9685 if ( not $extracted ) {
159 78         183 ( $extracted, $remaining_path ) = ( $remaining_path, undef );
160             }
161             else {
162 56         139 $remaining_path = ( chop $extracted ) . $remaining_path;
163             }
164 134         789 ( $key ) = $extracted =~ m,^/(.*),gs;
165 134         326 $key = _unescape $key;
166             }
167              
168 165         741 push @tokens, [ $key, $args ];
169             }
170 109         257 return \@tokens;
171             }
172              
173             sub _tokenize_args {
174 5     5   8 my $args = shift;
175 5         25 ( $args ) = $args =~ /^\((.*)\)$/;
176 14 100       510 return map { _unescape( $_ =~ /^['"]/ ? _unquote( $_ ) : $_ ) }
177             extract_multiple( $args, [
178             # quoted structures
179 23     23   8225 sub { extract_delimited( $_[0], q|'"| ) },
180             # handle unquoted bare words
181 5         66 qr/\s*(\w+)/s,
182             qr/\s*([^,]+)(.*)/s
183             ], undef, 1 );
184             }
185              
186             sub _spath {
187 109     109   341 my ( $data, $path, $opts ) = @_;
188              
189 109         134 my $current = $data;
190 109         195 my $depth = 0;
191 109         170 my $wantlist = wantarray;
192              
193 109         610 my $tokens = _tokenize( $path );
194              
195 109         134 for my $token ( @{ $tokens } ) {
  109         229  
196 165         198 $depth++;
197 165         194 my ( $key, $args ) = @{ $token };
  165         306  
198              
199 165 100       475 if ( blessed $current ) {
200              
201 14         16 my @args;
202 14 100       37 @args = _tokenize_args( $args )
203             if defined $args;
204              
205 14 100       171 return $opts->{method_miss}->( $key, $current, $depth )
206             unless my $method = $current->can( $key );
207              
208 12 50       23 if ( $wantlist ) {
209 0         0 my @current = $current->$method( @args );
210 0 0       0 $current = @current > 1 ? \@current : $current[0];
211             }
212             else {
213 12         93 $current = $current->$method( @args );
214             }
215             }
216             else {
217              
218 151 100       316 return $opts->{args_on_non_method}->( $key, $current, $args, $depth )
219             if defined $args;
220              
221 149         251 given ( ref $current ) {
222 149         319 when( 'HASH' ) {
223              
224 142 100       352 return $opts->{key_miss}->( $key, $current, $depth )
225             unless exists $current->{ $key };
226              
227 140         527 $current = $current->{ $key };
228             }
229 7         36 when ( 'ARRAY' ) {
230              
231 5 50       27 return $opts->{key_on_non_hash}->( $key, $current, $depth )
232             unless $key =~ /^\d+$/;
233 5         36 return $opts->{index_miss}->( $key, $current, $depth )
234 5 100       10 if $#{ $current } < $key;
235              
236 3         15 $current = $current->[ $key ];
237             }
238 2         3 default {
239 2         11 return $opts->{key_on_non_hash}->( $key, $current, $depth );
240             }
241             }
242             }
243             }
244 99         950 return $current;
245             }
246              
247              
248             1;
249              
250              
251             __END__