File Coverage

blib/lib/Hash/DotPath.pm
Criterion Covered Total %
statement 261 275 94.9
branch 98 176 55.6
condition n/a
subroutine 44 45 97.7
pod n/a
total 403 496 81.2


line stmt bran cond sub pod time code
1             package Hash::DotPath;
2             $Hash::DotPath::VERSION = '0.004';
3 7     7   939422 use Modern::Perl;
  7         77  
  7         62  
4 7     7   5000 use Moose;
  7         3281026  
  7         55  
5 7     7   58946 use namespace::autoclean;
  7         59541  
  7         32  
6 7     7   3739 use Kavorka 'method';
  7         74326  
  7         56  
7 7     7   1403234 use Data::Printer alias => 'pdump';
  7         17  
  7         90  
8 7     7   13688 use Hash::Merge;
  7         80291  
  7         8444  
9 7     7   3709 use Util::Medley::Hash;
  7         1368665  
  7         3006  
10              
11             with
12             'Util::Medley::Roles::Attributes::Hash',
13             'Util::Medley::Roles::Attributes::List',
14             'Util::Medley::Roles::Attributes::Logger',
15             'Util::Medley::Roles::Attributes::String';
16              
17             ########################################################
18              
19             =head1 NAME
20              
21             Hash::DotPath - Class for manipulating hashes via dot path notation.
22              
23             =head1 VERSION
24              
25             version 0.004
26              
27             =cut
28              
29             ########################################################
30              
31             =head1 SYNOPSIS
32              
33             $dot = Hash::DotPath->new;
34             $dot = Hash::DotPath->new(\%myhash);
35             $dot = Hash::DotPath->new(\%myhash, delimiter => '~');
36              
37             $val = $dot->get('foo.bar');
38             $val = $dot->get('biz.baz.0.zoo');
39              
40             $dot->set('foo', 'bar');
41             $dot->set('cats.0', 'calico');
42            
43             $dot->delete('foo');
44            
45             $newObj = $dot->merge({ biz => 'baz' });
46             $newObj = $dot->merge({ biz => 'other' }, 'RIGHT');
47              
48             %hash = $dot->toHash;
49             $href = $dot->toHashRef;
50            
51             =cut
52              
53             =head1 ARRAY vs HASH vivification
54              
55             When assigning a value to a path where a non-existent segment of the path is
56             an integer, an array reference will be vivified at that position. If you wish
57             to have a hash reference in its place, you must instantiate it manually in
58             advance. For example:
59              
60             # Assuming biz isn't defined yet, this will set biz to an array reference.
61            
62             $dot = Hash::DotPath->new;
63             $dot->set('biz.0', 'baz');
64             Data::Printer::p($dot->toHashRef);
65              
66             {
67             biz [
68             [0] "baz"
69             ]
70             }
71            
72             # In order to set biz to a hash reference you must instantiate it first.
73            
74             $dot->set('biz', {});
75             $dot->set('biz.0', 'baz');
76             Data::Printer::p($dot->toHashRef);
77            
78             {
79             biz {
80             0 "baz"
81             }
82             }
83            
84             =cut
85              
86             ##############################################################################
87             # PUBLIC ATTRIBUTES
88             ##############################################################################
89              
90             =head1 ATTRIBUTES
91              
92             =cut
93              
94             # this attrib is used indirectly. therefore, it isn't documented.
95             has init => (
96             is => 'rw',
97             isa => 'HashRef',
98             default => sub { {} },
99             );
100              
101             =head2 delimiter [Str] (optional)
102              
103             The delimiter to use when analyzing a dot path.
104              
105             Default: "."
106              
107             =cut
108              
109             has delimiter => (
110             is => 'rw',
111             isa => 'Str',
112             default => '.',
113             );
114              
115             ##############################################################################
116             # PRIVATE_ATTRIBUTES
117             ##############################################################################
118              
119             has _href => (
120             is => 'rw',
121             isa => 'HashRef',
122             );
123              
124             ##############################################################################
125             # CONSTRUCTOR
126             ##############################################################################
127              
128             around BUILDARGS => sub {
129              
130             my $orig = shift;
131             my $class = shift;
132              
133             my $href;
134             if (@_) {
135              
136             # TODO: is there a way to use the 'Hash' attrib instead?
137             my $util = Util::Medley::Hash->new;
138             if ( $util->isHash( $_[0] ) ) {
139             $href = shift @_;
140             }
141             }
142              
143             my %args = @_;
144             $args{init} = $href if $href;
145              
146             return $class->$orig(%args);
147             };
148              
149 7 50   7   10356 method BUILD {
  7     10   16  
  7         867  
  10         35  
  10         22  
150              
151 10         340 $self->_href( $self->init );
152             }
153              
154             ##############################################################################
155             # PUBLIC METHODS
156             ##############################################################################
157              
158             =head1 METHODS
159              
160             =head2 delete
161              
162             Deletes an element at the specified path. Returns the value of the element
163             that was deleted.
164              
165             =over
166              
167             =item usage:
168              
169             $val = $dot->delete('foo.bar');
170             $val = $dot->delete('biz.0.baz');
171              
172             =item args:
173              
174             =over
175              
176             =item path [Str]
177              
178             Dot-path of the element you wish to delete.
179              
180             =back
181              
182             =back
183              
184             =cut
185              
186 7 50   7   16747 method delete (Str $path!) {
  7 50   7   43  
  7 50   4   1217  
  7 50       54  
  7 50       20  
  7         2255  
  4         1026  
  4         12  
  4         10  
  4         9  
  4         9  
  4         14  
  4         7  
187              
188 4         133 my $ptr = $self->_href;
189 4         14 my @keys = $self->_splitKey($path);
190 4         7 my $lastKey = pop @keys;
191            
192 4 100       11 if (@keys) {
193 3         74 $ptr = $self->_get( $self->_href, \@keys );
194             }
195            
196 4         9 my $val;
197 4 100       144 if ( $self->List->isArray($ptr) ) {
198 2 100       717 if ( $self->String->isInt($lastKey) ) {
199 1         3166 $val = splice(@$ptr, $lastKey, 1);
200             }
201             else {
202 1         494 confess "can't reference array index at $path by $lastKey";
203             }
204             }
205             else {
206 2         3683 $val = $ptr->{$lastKey} ;
207 2         6 delete $ptr->{$lastKey};
208             }
209              
210 3         16 return $val; # --> Any
211             }
212              
213             =head2 get
214              
215             Gets an element at the specified path. Returns 'Any'.
216              
217             =over
218              
219             =item usage:
220              
221             $val = $dot->get('foo.bar');
222             $val = $dot->get('biz.0.baz');
223              
224             =item args:
225              
226             =over
227              
228             =item path [Str]
229              
230             Dot-path of the element you wish to get.
231              
232             =back
233              
234             =back
235              
236             =cut
237              
238 7 50   7   14767 method get (Str $path!) {
  7 50   7   52  
  7 50   18   1278  
  7 50       56  
  7 50       16  
  7         954  
  18         6083  
  18         60  
  18         59  
  18         49  
  18         24  
  18         63  
  18         27  
239              
240 18         49 my @keys = $self->_splitKey($path);
241              
242 18         459 return $self->_get( $self->_href, \@keys ); # --> Any
243             }
244              
245             =head2 exists
246              
247             Determines if an element exists at the given path. Returns 'Bool'.
248              
249             =over
250              
251             =item usage:
252              
253             $bool = $dot->exists('foo.bar');
254             $bool = $dot->exists('biz.0.baz');
255              
256             =item args:
257              
258             =over
259              
260             =item path [Str]
261              
262             Dot-path of the element you wish to get.
263              
264             =back
265              
266             =back
267              
268             =cut
269              
270 7 50   7   14543 method exists (Str $path!) {
  7 50   7   15  
  7 50   8   1275  
  7 50       55  
  7 50       35  
  7         990  
  8         30  
  8         24  
  8         24  
  8         17  
  8         15  
  8         25  
  8         11  
271              
272 8         20 my @keys = $self->_splitKey($path);
273              
274 8         205 return $self->_exists( $self->_href, \@keys ); # --> Bool
275             }
276              
277             =head2 merge
278              
279             Merges the provided dot-path object or hashref with the object. You indicate
280             which hash has precedence by providing the 'overwrite' arg.
281              
282             =over
283              
284             =item usage:
285              
286             $newDot = $dot->merge({foo => 'bar'}, [0|1]);
287              
288             $dot2 = Hash::DotPath->new(biz => 'baz');
289             $newDot = $dot->merge($dot2, [0|1]);
290            
291             =item args:
292              
293             =over
294              
295             =item merge [HashRef|Hash::DotPath]
296              
297             Hashref you wish to merge into the dot-path object.
298              
299             =item overwrite [Bool] (optional)
300              
301             Indicates which hash has precedence over the other. A true value means
302             the element passed in will overwrite any pre-existing elements. A false value
303             will preserve existing elements and just merge the new ones in.
304              
305             Default: 1
306              
307             =back
308              
309             =back
310              
311             =cut
312              
313             method merge (Object|HashRef $merge!,
314 7 50   7   65016 Bool $overwrite = 1) {
  7 50   7   20  
  7 50   7   1404  
  7 50   1   55  
  7 50       44  
  7 50       862  
  7 50       51  
  7 50       16  
  7         2182  
  1         6  
  1         4  
  1         4  
  1         3  
  1         5  
  1         2  
  1         9  
  1         8  
  1         3  
  1         4  
  1         3  
315              
316 1         2 my $href;
317 1 50       39 if ( $self->Hash->isHash($merge) ) {
318 1         452 $href = $merge;
319             }
320             else {
321 0         0 my $ref = ref($merge);
322 0 0       0 if ($ref eq 'Hash::DotPath') {
323 0         0 $href = $merge->toHashRef;
324             }
325             else {
326 0         0 confess "can't use $ref as a hashref";
327             }
328             }
329              
330 1         29 my %args = (left => $self->_href, right => $href);
331 1 50       6 $args{precedent} = 'RIGHT' if $overwrite;
332 1         49 my $merged = $self->Hash->merge(%args);
333            
334 1         4961 return __PACKAGE__->new( $merged, delimiter => $self->delimiter );
335             }
336              
337             =head2 set
338              
339             Sets an element at the specified path. Returns the value that was passed in.
340              
341             =over
342              
343             =item usage:
344              
345             $val = $dot->set('foo.bar', 'abc');
346             $val = $dot->set('biz.0.baz', 'def');
347              
348             =item args:
349              
350             =over
351              
352             =item path [Str]
353              
354             Dot-path of the element you wish to set.
355              
356             =item value [Any]
357              
358             Value you wish to set at the given path.
359              
360             =back
361              
362             =back
363              
364             =cut
365              
366             method set (Str $path!,
367 7 50   7   21393 Any $value!) {
  7 50   7   18  
  7 50   7   1421  
  7 50   7   59  
  7 50       16  
  7 50       1009  
  7 50       51  
  7         18  
  7         2296  
  7         3285  
  7         22  
  7         21  
  7         16  
  7         10  
  7         43  
  7         19  
  7         16  
  7         10  
  7         10  
  7         12  
368              
369 7         17 my @keys = $self->_splitKey($path);
370             # my $lastKey = pop @keys;
371             # my $parentPath = join $self->delimiter, @keys;
372              
373 7         185 my $ptr = $self->_buildParentPath( $self->_href, \@keys );
374            
375 7         14 my $lastKey = pop(@keys);
376 7 100       231 if ( $self->List->isArray($ptr) ) {
377 1 50       422 if ( $self->String->isInt($lastKey) ) {
378 0         0 $ptr->[$lastKey] = $value;
379             }
380             else {
381 1         424 my $parentPath = join($self->delimiter, @keys);
382 1         153 confess "can't reference array index at $parentPath by $lastKey";
383             }
384             }
385             else {
386 6         4941 $ptr->{$lastKey} = $value;
387             }
388              
389 6         23 return $value; # --> Any
390             }
391              
392             =head2 toHash
393              
394             Returns a hash version of the object.
395              
396             =over
397              
398             =item usage:
399              
400             %hash = $dot->toHash;
401              
402             =back
403              
404             =cut
405              
406 7 0   7   7779 method toHash {
  7     0   16  
  7         775  
  0         0  
  0         0  
407              
408 0         0 return %{ $self->_href };
  0         0  
409             }
410              
411             =head2 toHashRef
412              
413             Returns a hashref version of the object.
414              
415             =over
416              
417             =item usage:
418              
419             $href = $dot->toHashRef;
420              
421             =back
422              
423             =cut
424              
425 7 50   7   8069 method toHashRef {
  7     10   15  
  7         763  
  10         2357  
  10         17  
426              
427 10         288 return $self->_href;
428             }
429              
430             ##############################################################################
431             # PRIVATE METHODS
432             ##############################################################################
433              
434 7 50   7   15151 method _splitKey (Str $key) {
  7 50   7   19  
  7 50   37   1223  
  7 50       52  
  7 50       16  
  7         1071  
  37         81  
  37         115  
  37         87  
  37         106  
  37         46  
  37         94  
  37         52  
435              
436 37         1247 my $regex = sprintf '\%s', $self->delimiter;
437 37         189 my @split = split( /$regex/, $key );
438              
439 37         141 return @split;
440             }
441              
442             method _exists (HashRef $ptr,
443 7 50   7   22141 ArrayRef $keys) {
  7 50   7   18  
  7 50   7   1287  
  7 50   8   55  
  7 50       12  
  7 50       931  
  7 50       55  
  7 50       12  
  7         2670  
  8         21  
  8         22  
  8         19  
  8         21  
  8         10  
  8         23  
  8         34  
  8         17  
  8         13  
  8         15  
  8         12  
444              
445 8         21 my @remKeys = @$keys; # make a copy
446 8         16 my $currKey = shift @remKeys;
447              
448 8 50       214 if ( $self->List->isArray($ptr) ) {
449              
450 0 0       0 if ( $self->String->isInt($currKey) ) {
451 0 0       0 if (@remKeys) {
    0          
452 0         0 return $self->_get( $ptr->[$currKey], \@remKeys );
453             }
454             elsif ( exists $ptr->[$currKey] ) {
455 0         0 return 1;
456             }
457             }
458             }
459             else {
460              
461 8 100       5843 if ( exists $ptr->{$currKey} ) {
462 7 100       16 if (@remKeys) {
463 6         21 return $self->_get( $ptr->{$currKey}, \@remKeys );
464             }
465             else {
466 1         8 return 1;
467             }
468             }
469             }
470              
471 1         7 return 0;
472             }
473              
474             method _get (HashRef|ArrayRef $ptr,
475 7 50   7   32418 ArrayRef $keys) {
  7 50   7   16  
  7 50   7   1139  
  7 50   58   58  
  7 50       13  
  7 50       982  
  7 50       54  
  7 50       15  
  7         2477  
  58         135  
  58         137  
  58         146  
  58         126  
  58         84  
  58         200  
  58         122  
  58         129  
  58         106  
  58         133  
  58         79  
476              
477 58         147 my @remKeys = @$keys; # make a copy
478 58         106 my $currKey = shift @remKeys;
479              
480 58 100       1721 if ( $self->List->isArray($ptr) ) {
481              
482 11 100       4042 if ( $self->String->isInt($currKey) ) {
483 9 100       8939 if (@remKeys) {
484 4         27 return $self->_get( $ptr->[$currKey], \@remKeys );
485             }
486             else {
487 5         46 return $ptr->[$currKey];
488             }
489             }
490             }
491             else {
492              
493 47 100       19722 if ( exists $ptr->{$currKey} ) {
494 44 100       116 if (@remKeys) {
495 27         116 return $self->_get( $ptr->{$currKey}, \@remKeys );
496             }
497             else {
498 17         104 return $ptr->{$currKey};
499             }
500             }
501             }
502              
503 5         661 return; # not found (undef)
504             }
505              
506             method _buildParentPath (HashRef|ArrayRef $ptr,
507 7 50   7   26364 ArrayRef $keys) {
  7 50   7   14  
  7 50   7   1216  
  7 50   18   55  
  7 50       14  
  7 50       1034  
  7 50       49  
  7 50       15  
  7         3418  
  18         47  
  18         39  
  18         37  
  18         60  
  18         23  
  18         79  
  18         41  
  18         50  
  18         26  
  18         44  
  18         24  
508              
509 18         46 my @remKeys = @$keys;
510              
511 18 100       41 if (@remKeys > 1) {
512 11         19 my $currKey = shift @remKeys;
513 11         20 my $nextKey = $remKeys[0];
514              
515 11         15 my $nextRef;
516 11 100       343 if ($self->String->isInt($nextKey)) {
517 2         722 $nextRef = [];
518             }
519             else {
520 9         6041 $nextRef = {};
521             }
522            
523 11 100       351 if ( $self->List->isArray($ptr) ) {
    50          
524             # deref by index
525 2 50       802 if ( $self->String->isInt($currKey) ) {
526 2 50       699 if ( !exists $ptr->[$currKey] ) {
527 2         5 $ptr->[$currKey] = $nextRef;
528             }
529              
530 2         24 return $self->_buildParentPath( $ptr->[$currKey], \@remKeys );
531             }
532             else {
533 0         0 confess "can't reference an array index by $currKey";
534             }
535             }
536             elsif ( $self->Hash->isHash($ptr) ) {
537             # deref by hash key
538 9 100       6919 if ( !exists $ptr->{$currKey} ) {
539 5         15 $ptr->{$currKey} = $nextRef;
540             }
541              
542 9         40 return $self->_buildParentPath( $ptr->{$currKey}, \@remKeys );
543             }
544             }
545              
546 7         32 return $ptr;
547             }
548              
549             __PACKAGE__->meta->make_immutable;
550              
551             1;