File Coverage

blib/lib/JSON/Pointer.pm
Criterion Covered Total %
statement 183 192 95.3
branch 87 112 77.6
condition 20 26 76.9
subroutine 24 25 96.0
pod 11 11 100.0
total 325 366 88.8


line stmt bran cond sub pod time code
1             package JSON::Pointer;
2              
3 12     12   333454 use 5.008_001;
  12         38  
4 12     12   46 use strict;
  12         18  
  12         236  
5 12     12   50 use warnings;
  12         44  
  12         381  
6              
7 12     12   52 use B;
  12         16  
  12         581  
8 12     12   48 use Carp qw(croak);
  12         15  
  12         559  
9 12     12   5293 use Clone qw(clone);
  12         33348  
  12         779  
10 12     12   2321 use JSON qw(encode_json decode_json);
  12         52104  
  12         61  
11 12     12   8018 use JSON::Pointer::Context;
  12         25  
  12         391  
12 12     12   4523 use JSON::Pointer::Exception qw(:all);
  12         22  
  12         1625  
13 12     12   4696 use JSON::Pointer::Syntax qw(is_array_numeric_index);
  12         20  
  12         704  
14 12     12   5774 use URI::Escape qw(uri_unescape);
  12         13072  
  12         19525  
15              
16             our $VERSION = '0.07';
17              
18             sub traverse {
19 147     147 1 38399 my ($class, $document, $pointer, $opts) = @_;
20             $opts = +{
21             strict => 1,
22             inclusive => 0,
23 147 100       177 %{ $opts || +{} }
  147         719  
24             };
25 147         374 $pointer = uri_unescape($pointer);
26              
27 147         1142 my @tokens = JSON::Pointer::Syntax->tokenize($pointer);
28 147         781 my $context = JSON::Pointer::Context->new(+{
29             pointer => $pointer,
30             tokens => \@tokens,
31             target => $document,
32             parent => $document,
33             });
34              
35 147         264 foreach my $token (@tokens) {
36 197         396 $context->begin($token);
37              
38 197         378 my $parent = $context->parent;
39 197         675 my $type = ref $parent;
40              
41 197 100       348 if ($type eq "HASH") {
    50          
42 141 100       285 unless (exists $parent->{$token}) {
43 24         64 return _throw_or_return(ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE, $context, $opts->{strict});
44             }
45              
46 117         267 $context->next($parent->{$token});
47 117         196 next;
48             }
49             elsif ($type eq "ARRAY") {
50 56 100       112 if ($token eq '-') {
51 1         2 $token = $#{$parent} + 1;
  1         2  
52             }
53              
54 56         50 my $max_index = $#{$parent};
  56         89  
55 56 100       120 $max_index++ if $opts->{inclusive};
56              
57 56 100 100     134 if (is_array_numeric_index($token) && $token <= $max_index) {
58 44         127 $context->next($parent->[$token]);
59 44         92 next;
60             }
61             else {
62 12         42 return _throw_or_return(ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE, $context, $opts->{strict});
63             }
64             }
65             else {
66 0         0 return _throw_or_return(ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE, $context, $opts->{strict});
67             }
68             }
69              
70 111         251 $context->result(1);
71 111         692 return $context;
72             }
73              
74             sub get {
75 33     33 1 23207 my ($class, $document, $pointer, $strict) = @_;
76 33 100       93 $strict = 0 unless defined $strict;
77              
78 33         33 my $context;
79 33         41 eval {
80 33         110 $context = $class->traverse($document, $pointer, +{ strict => $strict });
81             };
82 33 100       254 if (my $e = $@) {
83 3         23 croak $e;
84             }
85              
86 30 100       57 return $context->result ? $context->target : undef;
87             }
88              
89             sub get_relative {
90 12     12 1 16430 my ($class, $document, $current_pointer, $relative_pointer, $strict) = @_;
91 12 100       39 $strict = 0 unless defined $strict;
92              
93 12         43 my @current_tokens = JSON::Pointer::Syntax->tokenize($current_pointer);
94              
95 12         78 my $context = JSON::Pointer::Context->new(+{
96             pointer => $current_pointer,
97             tokens => \@current_tokens,
98             target => $document,
99             parent => $document,
100             });
101              
102 12         63 my ($steps, $relative_pointer_suffix, $use_index) =
103             ($relative_pointer =~ m{^(0|[1-9]?[0-9]+)([^#]*)(#?)$});
104 12   100     31 $relative_pointer_suffix ||= "";
105              
106 12 100       23 unless (defined $steps) {
107 1         3 return _throw_or_return(ERROR_INVALID_POINTER_SYNTAX, $context, +{ strict => $strict });
108             }
109              
110 11         32 for (my $i = 0; $i < $steps; $i++) {
111 11 100       19 if (@current_tokens == 0) {
112 1         3 return _throw_or_return(ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE, $context, +{ strict => $strict });
113             }
114 10         22 pop(@current_tokens);
115             }
116              
117 10 100       18 if ($use_index) {
118 4         11 my @relative_tokens = JSON::Pointer::Syntax->tokenize($relative_pointer_suffix);
119 4 50       23 return (@relative_tokens > 0) ? $relative_tokens[-1] : $current_tokens[-1];
120             }
121              
122 6         17 my $absolute_pointer = JSON::Pointer::Syntax->as_pointer(@current_tokens) . $relative_pointer_suffix;
123              
124 6         9 eval {
125 6         19 $context = $class->traverse($document, $absolute_pointer, +{ strict => $strict });
126             };
127 6 50       33 if (my $e = $@) {
128 0         0 croak $e;
129             }
130              
131 6 50       11 return $context->result ? $context->target : undef;
132             }
133              
134             sub contains {
135 9     9 1 1944 my ($class, $document, $pointer) = @_;
136 9         24 my $context = $class->traverse($document, $pointer, +{ strict => 0 });
137 9         21 return $context->result;
138             }
139              
140             sub add {
141 34     34 1 27605 my ($class, $document, $pointer, $value) = @_;
142              
143 34         248 my $patched_document = clone($document);
144              
145 34         112 my $context = $class->traverse($patched_document, $pointer, +{ strict => 0, inclusive => 1 });
146 34         80 my $parent = $context->parent;
147 34         96 my $type = ref $parent;
148              
149 34 100       72 if ($type eq "HASH") {
    50          
150 21 100 100     60 if (!$context->result && @{$context->processed_tokens} < @{$context->tokens} - 1) {
  19         91  
  19         70  
151             ### Parent isn't object
152 2         11 JSON::Pointer::Exception->throw(
153             code => ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE,
154             context => $context,
155             );
156             }
157              
158 19 100       113 if (defined $context->last_token) {
159 18         75 $parent->{$context->last_token} = $value;
160             }
161             else {
162             ### pointer is empty string (whole document)
163 1         4 $patched_document = $value;
164             }
165              
166 19         132 return $patched_document;
167             }
168             elsif ($type eq "ARRAY") {
169 13 100       26 unless ($context->result) {
170 3         53 JSON::Pointer::Exception->throw(
171             code => ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE,
172             context => $context,
173             );
174             }
175              
176 10 50       44 if (defined $context->last_token) {
177 10         30 my $parent_array_length = $#{$parent} + 1;
  10         55  
178 10 100       18 my $target_index = ($context->last_token eq "-") ?
179             $parent_array_length : $context->last_token;
180              
181 10         69 splice(@$parent, $target_index, 0, $value);
182             }
183             else {
184 0         0 $patched_document = $value;
185             }
186              
187 10         41 return $patched_document;
188             }
189             else {
190 0 0       0 unless ($context->result) {
191 0         0 JSON::Pointer::Exception->throw(
192             code => ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE,
193             context => $context,
194             );
195             }
196              
197 0         0 return $value;
198             }
199             }
200              
201             sub remove {
202 14     14 1 14173 my ($class, $document, $pointer) = @_;
203              
204 14         132 my $patched_document = clone($document);
205              
206 14         41 my $context = $class->traverse($patched_document, $pointer);
207 13         20 my $parent = $context->parent;
208 13         38 my $type = ref $parent;
209              
210 13 100       29 if ($type eq "HASH") {
    100          
211 8         16 my $target_member = $context->last_token;
212 8 100       27 if (defined $target_member) {
213 7         15 my $removed = delete $parent->{$target_member};
214 7 50       49 return wantarray ? ($patched_document, $removed) : $patched_document;
215             }
216             else {
217             ### pointer is empty string (whole document)
218 1 50       6 return wantarray ? (undef, $patched_document) : undef;
219             }
220             }
221             elsif ($type eq "ARRAY") {
222 4         9 my $target_index = $context->last_token;
223 4 100       17 if (defined $target_index) {
224 3         2 my $parent_array_length = $#{$parent} + 1;
  3         8  
225 3 50       8 $target_index = $parent_array_length if ($target_index eq "-");
226 3         8 my $removed = splice(@$parent, $target_index, 1);
227 3 50       19 return wantarray ? ($patched_document, $removed) : $patched_document;
228             }
229             else {
230             ### pointer is empty string (whole document)
231 1 50       6 return wantarray ? (undef, $patched_document) : undef;
232             }
233             }
234             else {
235 1 50       3 unless ($context->result) {
236 0         0 JSON::Pointer::Exception->throw(
237             code => ERROR_POINTER_REFERENCES_NON_EXISTENT_VALUE,
238             context => $context,
239             );
240             }
241              
242 1 50       12 return wantarray ? (undef, $patched_document) : undef;
243             }
244             }
245              
246             sub replace {
247 12     12 1 21826 my ($class, $document, $pointer, $value) = @_;
248              
249 12         89 my $patched_document = clone($document);
250 12         27 my $context = $class->traverse($patched_document, $pointer);
251 12         21 my $parent = $context->parent;
252 12         33 my $type = ref $parent;
253              
254 12 100       18 if ($type eq "HASH") {
255 4         9 my $target_member = $context->last_token;
256 4 100       13 if (defined $target_member) {
257 3         7 my $replaced = $parent->{$context->last_token};
258 3         11 $parent->{$context->last_token} = $value;
259 3 50       26 return wantarray ? ($patched_document, $replaced) : $patched_document;
260             }
261             else {
262             ### pointer is empty string (whole document)
263 1 50       6 return wantarray ? ($value, $patched_document) : $value;
264             }
265             }
266             else {
267 8         15 my $target_index = $context->last_token;
268 8 100       26 if (defined $target_index) {
269 6         5 my $parent_array_length = $#{$parent} + 1;
  6         11  
270 6 50       11 $target_index = $parent_array_length if ($target_index eq "-");
271 6         9 my $replaced = $parent->[$target_index];
272 6         7 $parent->[$target_index] = $value;
273 6 50       33 return wantarray ? ($patched_document, $replaced) : $patched_document;
274             }
275             else {
276             ### pointer is empty string (whole document)
277 2 50       10 return wantarray ? ($value, $patched_document) : $value;
278             }
279             }
280             }
281              
282             sub set {
283 0     0 1 0 shift->replace(@_);
284             }
285              
286             sub copy {
287 1     1 1 864 my ($class, $document, $from_pointer, $to_pointer) = @_;
288 1         4 my $context = $class->traverse($document, $from_pointer);
289 1         3 return $class->add($document, $to_pointer, $context->target);
290             }
291              
292             sub move {
293 5     5 1 7250 my ($class, $document, $from_pointer, $to_pointer) = @_;
294 5         14 my ($patched_document, $removed) = $class->remove($document, $from_pointer);
295 5         12 $class->add($patched_document, $to_pointer, $removed);
296             }
297              
298             sub test {
299 23     23 1 24753 my ($class, $document, $pointer, $value) = @_;
300              
301 23         74 my $context = $class->traverse($document, $pointer, +{ strict => 0 });
302              
303 23 100       48 return 0 unless $context->result;
304              
305 22         88 my $target = $context->target;
306 22         51 my $target_type = ref $target;
307              
308 22 100 100     83 if ($target_type eq "HASH" || $target_type eq "ARRAY") {
    100          
309 4 100       37 return encode_json($target) eq encode_json($value) ? 1 : 0;
310             }
311             elsif (defined $target) {
312 17 50 100     39 if (JSON::is_bool($target)) {
    100 66        
    100          
313 0 0 0     0 return JSON::is_bool($value) && $target == $value ? 1 : 0;
314             }
315             elsif (_is_iv_or_nv($target) && _is_iv_or_nv($value)) {
316 12 50       49 return $target == $value ? 1 : 0;
317             }
318             elsif (_is_pv($target) && _is_pv($value)) {
319 4 100       25 return $target eq $value ? 1 : 0;
320             }
321             else {
322 1         4 return 0;
323             }
324             }
325             else {
326             ### null
327 1 50       8 return !defined $value ? 1 : 0;
328             }
329             }
330              
331             sub _throw_or_return {
332 38     38   53 my ($code, $context, $strict) = @_;
333              
334 38 100       71 if ($strict) {
335 6         37 JSON::Pointer::Exception->throw(
336             code => $code,
337             context => $context,
338             );
339             }
340             else {
341 32         67 $context->last_error($code);
342 32         176 return $context;
343             }
344             }
345              
346             sub _is_iv_or_nv {
347 30     30   79 my $value = shift;
348 30         98 my $flags = B::svref_2object(\$value)->FLAGS;
349 30   66     138 return ( ($flags & ( B::SVp_IOK | B::SVp_NOK )) && !($flags & B::SVp_POK) );
350             }
351              
352             sub _is_pv {
353 9     9   8 my $value = shift;
354 9         21 my $flags = B::svref_2object(\$value)->FLAGS;
355 9   66     39 return ( !($flags & ( B::SVp_IOK | B::SVp_NOK )) && ($flags & B::SVp_POK) );
356             }
357              
358             1;
359              
360             __END__