File Coverage

blib/lib/Catmandu/Path/simple.pm
Criterion Covered Total %
statement 165 193 85.4
branch 47 60 78.3
condition 15 15 100.0
subroutine 17 18 94.4
pod 3 4 75.0
total 247 290 85.1


line stmt bran cond sub pod time code
1             package Catmandu::Path::simple;
2              
3 95     95   104377 use Catmandu::Sane;
  95         337  
  95         896  
4              
5             our $VERSION = '1.2020';
6              
7             use Catmandu::Util
8 95     95   881 qw(is_hash_ref is_array_ref is_value is_natural is_code_ref trim);
  95         246  
  95         7464  
9 95     95   638 use Moo;
  95         248  
  95         764  
10 95     95   42193 use namespace::clean;
  95         278  
  95         1171  
11              
12             with 'Catmandu::Path', 'Catmandu::Emit';
13              
14 95     95   45250 use overload '""' => sub {$_[0]->path};
  95     0   274  
  95         1125  
  0         0  
15              
16             sub split_path {
17 629     629 0 1220 my ($self) = @_;
18 629         1547 my $path = $self->path;
19 629 50       1957 if (is_value($path)) {
20 629         1905 $path = trim($path);
21 629         1356 $path =~ s/^\$[\.\/]//;
22 629         2399 $path = [map {s/\\(?=[\.\/])//g; $_} split /(?<!\\)[\.\/]/, $path];
  807         1434  
  807         2285  
23 629         1735 return $path;
24             }
25 0 0       0 if (is_array_ref($path)) {
26 0         0 return $path;
27             }
28 0         0 Catmandu::Error->throw("path should be a string or arrayref of strings");
29             }
30              
31             sub getter {
32 175     175 1 35061 my ($self) = @_;
33 175         465 my $path = $self->split_path;
34 175         617 my $data_var = $self->_generate_var;
35 175         464 my $vals_var = $self->_generate_var;
36              
37             my $body = $self->_emit_declare_vars($vals_var, '[]') . $self->_emit_get(
38             $data_var,
39             $path,
40             sub {
41 182     182   607 my ($var, %opts) = @_;
42              
43             # looping goes backwards to keep deletions safe
44 182         971 "unshift(\@{${vals_var}}, ${var});";
45             },
46 175         619 ) . "return ${vals_var};";
47              
48 175         1195 $self->_eval_sub($body, args => [$data_var]);
49             }
50              
51             sub setter {
52 71     71 1 6959 my $self = shift;
53 71 50       292 my %opts = @_ == 1 ? (value => $_[0]) : @_;
54 71         192 my $path = $self->split_path;
55 71         157 my $key = pop @$path;
56 71         215 my $data_var = $self->_generate_var;
57 71         194 my $val_var = $self->_generate_var;
58 71         142 my $captures = {};
59 71         167 my $args = [$data_var];
60              
61             my $body = $self->_emit_get(
62             $data_var,
63             $path,
64             sub {
65 71     71   149 my $var = $_[0];
66 71         109 my $val;
67 71 100       337 if (is_code_ref($opts{value})) {
    50          
68 10         26 $captures->{$val_var} = $opts{value};
69 10         33 $val = "${val_var}->(${var}, ${data_var})";
70             }
71             elsif (exists $opts{value}) {
72 61         181 $captures->{$val_var} = $opts{value};
73 61         114 $val = $val_var;
74             }
75             else {
76 0         0 push @$args, $val_var;
77 0         0 $val
78             = "is_code_ref(${val_var}) ? ${val_var}->(${var}, ${data_var}) : ${val_var}";
79             }
80              
81 71         215 $self->_emit_set_key($var, $key, $val);
82             },
83 71         468 ) . "return ${data_var};";
84              
85 71         585 $self->_eval_sub($body, args => $args, captures => $captures);
86             }
87              
88             sub updater {
89             my ($self, %opts) = @_;
90             my $path = $self->split_path;
91             my $data_var = $self->_generate_var;
92             my $captures = {};
93             my $args = [$data_var];
94             my $cb;
95              
96             if (my $tests = $opts{if}) {
97             $cb = sub {
98             my ($var, %opts) = @_;
99             my $perl = "";
100             for (my $i = 0; $i < @$tests; $i += 2) {
101             my $test = $tests->[$i];
102             my $val = $tests->[$i + 1];
103             my $test_var = $self->_generate_var;
104             my $val_var = $self->_generate_var;
105             $captures->{$test_var} = $test;
106             $captures->{$val_var} = $val;
107             if ($i) {
108             $perl .= 'els';
109             }
110             $perl
111             .= "if (List::Util::any {\$_->(${var})} \@{${test_var}}) {"
112             . $self->_emit_assign_cb($var, $val_var, %opts) . '}';
113             }
114             $perl;
115             };
116             }
117             else {
118             my $val_var = $self->_generate_var;
119             if (my $val = $opts{value}) {
120             $captures->{$val_var} = $val;
121             }
122             else {
123             push @$args, $val_var;
124             }
125             $cb = sub {
126             my ($var, %opts) = @_;
127             $self->_emit_assign_cb($var, $val_var, %opts);
128             };
129             }
130              
131             my $body
132             = $self->_emit_get($data_var, $path, $cb) . "return ${data_var};";
133              
134             $self->_eval_sub($body, args => $args, captures => $captures);
135             }
136              
137             sub creator {
138             my ($self, %opts) = @_;
139             my $path = $self->split_path;
140             my $data_var = $self->_generate_var;
141             my $val_var = $self->_generate_var;
142             my $captures = {};
143             my $args = [$data_var];
144             my $cb;
145              
146             if (is_code_ref($opts{value})) {
147             $captures->{$val_var} = $opts{value};
148             $cb = sub {
149             my $var = $_[0];
150             "${var} = ${val_var}->(${var}, ${data_var});";
151             };
152             }
153             elsif (exists $opts{value}) {
154             $captures->{$val_var} = $opts{value};
155             $cb = sub {
156             my $var = $_[0];
157             "${var} = ${val_var};";
158             };
159             }
160             else {
161             push @$args, $val_var;
162             $cb = sub {
163             my $var = $_[0];
164             "if (is_code_ref(${val_var})) {"
165             . "${var} = ${val_var}->(${var}, ${data_var});"
166             . '} else {'
167             . "${var} = ${val_var};" . '}';
168             };
169             }
170              
171             my $body = $self->_emit_create_path($data_var, $path, $cb);
172              
173             $body .= "return ${data_var};";
174              
175             $self->_eval_sub($body, args => $args, captures => $captures);
176             }
177              
178             sub deleter {
179 41     41 1 1371 my ($self) = @_;
180 41         99 my $path = $self->split_path;
181 41         92 my $key = pop @$path;
182 41         132 my $data_var = $self->_generate_var;
183              
184             my $body = $self->_emit_get(
185             $data_var,
186             $path,
187             sub {
188 41     41   80 my $var = $_[0];
189 41         101 $self->_emit_delete_key($var, $key);
190             }
191 41         193 ) . "return ${data_var};";
192              
193 41         217 $self->_eval_sub($body, args => [$data_var]);
194             }
195              
196             sub _emit_get {
197 884     884   2698 my ($self, $var, $path, $cb, %opts) = @_;
198              
199 884 100       2760 @$path || return $cb->($var, %opts);
200              
201 438         1050 $path = [@$path];
202              
203 438         905 my $key = shift @$path;
204 438         1375 my $str_key = $self->_emit_string($key);
205 438         825 my $perl = "";
206              
207 438         1255 %opts = (up_var => my $up_var = $var);
208 438         1087 $var = $self->_generate_var;
209              
210 438 100       1380 if (is_natural($key)) {
    100          
211 8         28 $perl
212             .= "if (is_hash_ref(${up_var}) && exists(${up_var}->{${str_key}})) {";
213 8         22 $perl .= "my ${var} = ${up_var}->{${str_key}};";
214 8         32 $perl .= $self->_emit_get($var, $path, $cb, %opts, key => $str_key);
215 8         32 $perl
216             .= "} elsif (is_array_ref(${up_var}) && \@{${up_var}} > ${key}) {";
217 8         24 $perl .= "my ${var} = ${up_var}->[${key}];";
218 8         25 $perl .= $self->_emit_get($var, $path, $cb, %opts, index => $key);
219 8         21 $perl .= "}";
220             }
221             elsif ($key eq '*') {
222 37         154 $perl .= "if (is_array_ref(${up_var})) {";
223             $perl .= $self->_emit_iterate_array(
224             $up_var,
225             sub {
226 37     37   147 my ($v, %opts) = @_;
227 37         343 "my ${var} = ${v};"
228             . $self->_emit_get($var, $path, $cb, %opts);
229             }
230 37         402 );
231 37         218 $perl .= "}";
232             }
233             else {
234 393 100       1236 if ($key eq '$first') {
    100          
235 1         12 $opts{index} = 0;
236 1         6 $perl .= "if (is_array_ref(${up_var}) && \@{${up_var}}) {";
237 1         3 $perl .= "my ${var} = ${up_var}->[0];";
238             }
239             elsif ($key eq '$last') {
240 1         4 $opts{index} = my $i = $self->_generate_var;
241 1         6 $perl .= "if (is_array_ref(${up_var}) && \@{${up_var}}) {";
242 1         6 $perl .= $self->_emit_declare_vars($i, "\@{${up_var}} - 1");
243 1         3 $perl .= "my ${var} = ${up_var}->[${i}];";
244             }
245             else {
246 391         881 $opts{key} = $str_key;
247 391         1311 $perl
248             .= "if (is_hash_ref(${up_var}) && exists(${up_var}->{${str_key}})) {";
249 391         1176 $perl .= "my ${var} = ${up_var}->{${str_key}};";
250             }
251 393         2460 $perl .= $self->_emit_get($var, $path, $cb, %opts);
252 393         869 $perl .= "}";
253             }
254              
255 438         1929 $perl;
256             }
257              
258             sub _emit_set_key {
259 71     71   215 my ($self, $var, $key, $val) = @_;
260              
261 71 50       219 return "${var} = $val;" unless defined $key;
262              
263 71         138 my $perl = "";
264 71         236 my $str_key = $self->_emit_string($key);
265              
266 71 50       205 if (is_natural($key)) {
    50          
    50          
    50          
    50          
    50          
267 0         0 $perl .= "if (is_hash_ref(${var})) {";
268 0         0 $perl .= "${var}->{${str_key}} = $val;";
269 0         0 $perl .= "} elsif (is_array_ref(${var})) {";
270 0         0 $perl .= "${var}->[${key}] = $val;";
271 0         0 $perl .= "}";
272             }
273             elsif ($key eq '$first') {
274 0         0 $perl .= "if (is_array_ref(${var})) {";
275 0         0 $perl .= "${var}->[0] = $val;";
276 0         0 $perl .= "}";
277             }
278             elsif ($key eq '$last') {
279 0         0 $perl .= "if (is_array_ref(${var})) {";
280 0         0 $perl .= "${var}->[\@{${var}} - 1] = $val;";
281 0         0 $perl .= "}";
282             }
283             elsif ($key eq '$prepend') {
284 0         0 $perl .= "if (is_array_ref(${var})) {";
285 0         0 $perl .= "unshift(\@{${var}}, $val);";
286 0         0 $perl .= "}";
287             }
288             elsif ($key eq '$append') {
289 0         0 $perl .= "if (is_array_ref(${var})) {";
290 0         0 $perl .= "push(\@{${var}}, $val);";
291 0         0 $perl .= "}";
292             }
293             elsif ($key eq '*') {
294 0         0 my $i = $self->_generate_var;
295 0         0 $perl .= "if (is_array_ref(${var})) {";
296 0         0 $perl .= "for (my ${i} = 0; ${i} < \@{${var}}; ${i}++) {";
297 0         0 $perl .= "${var}->[${i}] = $val;";
298 0         0 $perl .= "}}";
299             }
300             else {
301 71         232 $perl .= "if (is_hash_ref(${var})) {";
302 71         204 $perl .= "${var}->{${str_key}} = $val;";
303 71         135 $perl .= "}";
304             }
305              
306 71         333 $perl;
307             }
308              
309             sub _emit_create_path {
310 465     465   1056 my ($self, $var, $path, $cb) = @_;
311              
312 465 100       1210 @$path || return $cb->($var);
313              
314 264         464 my $key = shift @$path;
315 264         806 my $str_key = $self->_emit_string($key);
316 264         493 my $perl = "";
317              
318 264 100       636 if (is_natural($key)) {
    100          
319 9         37 my $v1 = $self->_generate_var;
320 9         22 my $v2 = $self->_generate_var;
321 9         33 $perl .= "if (is_hash_ref(${var})) {";
322 9         30 $perl .= "my ${v1} = ${var};";
323 9         50 $perl
324             .= $self->_emit_create_path("${v1}->{${str_key}}", [@$path], $cb);
325 9         36 $perl .= "} elsif (is_maybe_array_ref(${var})) {";
326 9         24 $perl .= "my ${v2} = ${var} //= [];";
327 9         39 $perl .= $self->_emit_create_path("${v2}->[${key}]", [@$path], $cb);
328 9         23 $perl .= "}";
329             }
330             elsif ($key eq '*') {
331 1         8 my $v1 = $self->_generate_var;
332 1         7 my $v2 = $self->_generate_var;
333 1         7 $perl .= "if (is_array_ref(${var})) {";
334 1         4 $perl .= "my ${v1} = ${var};";
335              
336             # loop backwards so that deletions are safe
337 1         8 $perl .= "for (my ${v2} = \@{${v1}} - 1; $v2 >= 0; ${v2}--) {";
338 1         4 $perl .= $self->_emit_create_path("${v1}->[${v2}]", $path, $cb);
339 1         3 $perl .= "}";
340 1         3 $perl .= "}";
341             }
342             else {
343 254         654 my $v = $self->_generate_var;
344 254 100 100     1796 if ( $key eq '$first'
      100        
      100        
345             || $key eq '$last'
346             || $key eq '$prepend'
347             || $key eq '$append')
348             {
349 26         75 $perl .= "if (is_maybe_array_ref(${var})) {";
350 26         72 $perl .= "my ${v} = ${var} //= [];";
351 26 100       173 if ($key eq '$first') {
    100          
    100          
    50          
352 1         5 $perl .= $self->_emit_create_path("${v}->[0]", $path, $cb);
353             }
354             elsif ($key eq '$last') {
355 1         13 $perl .= "if (\@${v}) {";
356 1         8 $perl .= $self->_emit_create_path("${v}->[\@${v} - 1]",
357             [@$path], $cb);
358 1         6 $perl .= "} else {";
359 1         7 $perl .= $self->_emit_create_path("${v}->[0]", [@$path], $cb);
360 1         13 $perl .= "}";
361             }
362             elsif ($key eq '$prepend') {
363 2         8 $perl .= "if (\@${v}) {";
364 2         7 $perl .= "unshift(\@${v}, undef);";
365 2         7 $perl .= "}";
366 2         8 $perl .= $self->_emit_create_path("${v}->[0]", $path, $cb);
367             }
368             elsif ($key eq '$append') {
369 22         65 my $index_var = $self->_generate_var;
370 22         91 $perl
371             .= $self->_emit_declare_vars($index_var, "scalar(\@${v})")
372             . $self->_emit_create_path("${v}->[${index_var}]", $path,
373             $cb);
374             }
375 26         59 $perl .= "}";
376             }
377             else {
378 228         680 $perl .= "if (is_maybe_hash_ref(${var})) {";
379 228         595 $perl .= "my ${v} = ${var} //= {};";
380 228         1299 $perl
381             .= $self->_emit_create_path("${v}->{${str_key}}", $path, $cb);
382 228         606 $perl .= "}";
383             }
384             }
385              
386 264         648 $perl;
387             }
388              
389             sub _emit_delete_key {
390 41     41   88 my ($self, $var, $key) = @_;
391              
392 41         103 my $str_key = $self->_emit_string($key);
393 41         81 my $perl = "";
394              
395 41 100 100     91 if (is_natural($key)) {
    100 100        
396 7         29 $perl .= "if (is_hash_ref(${var}) && exists(${var}->{${str_key}})) {";
397 7         18 $perl .= "delete(${var}->{${str_key}});";
398 7         20 $perl .= "} elsif (is_array_ref(${var}) && \@{${var}} > ${key}) {";
399 7         21 $perl .= "splice(\@{${var}}, ${key}, 1)";
400             }
401             elsif ($key eq '$first' || $key eq '$last' || $key eq '*') {
402 3         10 $perl .= "if (is_array_ref(${var}) && \@{${var}}) {";
403 3 100       10 $perl .= "splice(\@{${var}}, 0, 1)" if $key eq '$first';
404 3 100       10 $perl .= "splice(\@{${var}}, \@{${var}} - 1, 1)" if $key eq '$last';
405 3 100       9 $perl .= "splice(\@{${var}}, 0, \@{${var}})" if $key eq '*';
406             }
407             else {
408 31         148 $perl .= "if (is_hash_ref(${var})) {";
409 31         73 $perl .= "delete(${var}->{${str_key}})";
410             }
411 41         78 $perl .= ";";
412 41         67 $perl .= "}";
413              
414 41         161 $perl;
415             }
416              
417             1;
418              
419             __END__
420              
421             =pod
422              
423             =head1 NAME
424              
425             Catmandu::Path::simple - The default Catmandu path syntax
426              
427             =head1 SYNOPSIS
428              
429             my $data = {foo => {bar => ['first_bar', 'second_bar']}};
430              
431             my $path = Catmandu::Path::simple->new("foo.bar.0");
432              
433             my $getter = $path->getter;
434             my $first_bar = $getter->($data);
435              
436             my $updater = $path->updater(sub { my $str = $_[0]; uc $str });
437             $updater->($data);
438             # => {foo => {bar => ['FIRST_BAR', 'second_bar']}}
439              
440             # safer version with a type check
441             my $updater = $path->updater(if_string => sub { my $str = $_[0]; uc $str });
442              
443             =head1 CONFIGURATION
444              
445             =over 4
446              
447             =item path
448              
449             The string version of the path. Required.
450              
451             =back
452              
453             =head1 METHODS
454              
455             =head2 getter
456              
457             Returns a coderef that can get the values for the path.
458             The coderef takes the data as argument and returns the matching values as an
459             arrayref.
460              
461             my $path = Catmandu::Path::Simple->new(path => '$.foo');
462             my $data = {foo => 'foo', bar => 'bar'};
463             $path->getter->($data);
464             # => ['foo']
465              
466             =head2 setter
467              
468             Returns a coderef that can create the final part of the path and set it's
469             value. In contrast to C<creator> this will only set the value if the
470             intermediate path exists. The coderef takes the data as argument and also
471             returns the data.
472              
473             my $path = Catmandu::Path::Simple->new(path => '$.foo.$append');
474             $path->creator(value => 'foo')->({});
475             # => {foo => ['foo']}
476             $path->creator(value => sub { my ($val, $data) = @_; $val // 'foo' })->({});
477             # => {foo => ['foo']}
478              
479             # calling creator with no value creates a sub that takes the value as an
480             # extra argument
481             $path->creator->({}, 'foo');
482             $path->creator->({}, sub { my ($val, $data) = @_; $val // 'foo' });
483             # => {foo => ['foo']}
484              
485             =head2 setter(\&callback|$value)
486              
487             This is a shortcut for C<setter(value => \&callback|$value)>.
488              
489             =head2 updater(value => \&callback)
490              
491             Returns a coderef that can update the value of an existing path.
492              
493             =head2 updater(if_* => [\&callback])
494              
495             TODO
496              
497             =head2 updater(if => [\&callback])
498              
499             TODO
500              
501             =head2 updater(if_* => \&callback)
502              
503             TODO
504              
505             =head2 updater(if => \&callback)
506              
507             TODO
508              
509             =head2 updater(\&callback)
510              
511             This is a shortcut for C<updater(value => \&callback|$value)>.
512              
513             =head2 creator(value => \&callback|$value)
514              
515             Returns a coderef that can create the path and set it's value. In contrast to
516             C<setter> this also creates the intermediate path if necessary.
517             The coderef takes the data as argument and also returns the data.
518              
519             my $path = Catmandu::Path::Simple->new(path => '$.foo.$append');
520             $path->creator(value => 'foo')->({});
521             # => {foo => ['foo']}
522             $path->creator(value => sub { my ($val, $data) = @_; $val // 'foo' })->({});
523             # => {foo => ['foo']}
524              
525             # calling creator with no value creates a sub that takes the value as an
526             # extra argument
527             $path->creator->({}, 'foo');
528             $path->creator->({}, sub { my ($val, $data) = @_; $val // 'foo' });
529             # => {foo => ['foo']}
530              
531             =head2 creator(\&callback|$value)
532              
533             This is a shortcut for C<creator(value => \&callback|$value)>.
534              
535             =head2 deleter
536              
537             Returns a coderef that can delete the path.
538             The coderef takes the data as argument and also returns the data.
539              
540             my $path = Catmandu::Path::Simple->new(path => '$.foo');
541             $path->deleter->({foo => 'foo', bar => 'bar'});
542             # => {bar => 'bar'}
543              
544             =head1 SEE ALSO
545              
546             L<Catmandu::Path>.
547              
548             =cut