File Coverage

blib/lib/Hash/Layout.pm
Criterion Covered Total %
statement 232 252 92.0
branch 94 142 66.2
condition 53 81 65.4
subroutine 36 40 90.0
pod 21 22 95.4
total 436 537 81.1


line stmt bran cond sub pod time code
1             package Hash::Layout;
2 5     5   26825 use strict;
  5         12  
  5         198  
3 5     5   24 use warnings;
  5         11  
  5         231  
4              
5             # ABSTRACT: hashes with predefined layouts, composite keys and default values
6              
7             our $VERSION = '1.02';
8              
9 5     5   4342 use Moo;
  5         103833  
  5         38  
10 5     5   49960 use Types::Standard qw(:all);
  5         541278  
  5         65  
11 5     5   191682 use Scalar::Util qw(blessed looks_like_number);
  5         15  
  5         375  
12 5     5   5211 use Hash::Merge::Simple 'merge';
  5         2506  
  5         281  
13 5     5   3414 use Clone;
  5         17404  
  5         280  
14              
15 5     5   3062 use Hash::Layout::Level;
  5         32  
  5         30490  
16              
17             has 'levels', is => 'ro', isa => ArrayRef[
18             InstanceOf['Hash::Layout::Level']
19             ], required => 1, coerce => \&_coerce_levels_param;
20              
21 263     263 1 333 sub num_levels { scalar(@{(shift)->levels}) }
  263         761  
22              
23             has 'default_value', is => 'ro', default => sub { 1 };
24             has 'default_key', is => 'ro', isa => Str, default => sub { '*' };
25             has 'allow_deep_values', is => 'ro', isa => Bool, default => sub { 1 };
26             has 'deep_delimiter', is => 'ro', isa => Str, default => sub { '.' };
27             has 'no_fill', is => 'ro', isa => Bool, default => sub { 0 };
28             has 'no_pad', is => 'ro', isa => Bool, default => sub { 0 };
29              
30             has 'lookup_mode', is => 'rw', isa => Enum[qw(get fallback merge)],
31             default => sub { 'merge' };
32              
33             has '_Hash', is => 'ro', isa => HashRef, default => sub {{}}, init_arg => undef;
34             has '_all_level_keys', is => 'ro', isa => HashRef, default => sub {{}}, init_arg => undef;
35              
36             # List of bitmasks representing every key path which includes
37             # a default_key, with each bit representing the level and '1' toggled on
38             # where the key is the default
39             has '_def_key_bitmasks', is => 'ro', isa => HashRef, default => sub {{}}, init_arg => undef;
40              
41 242     242 1 10150 sub Data { Clone::clone( (shift)->_Hash ) }
42              
43             sub level_keys {
44 9     9 1 13 my ($self, $index) = @_;
45 9 50       33 die 'level_keys() expects level index argument'
46             unless (looks_like_number $index);
47            
48 9 50       24 die "No such level index '$index'"
49             unless ($self->levels->[$index]);
50              
51 9   50     55 return $self->_all_level_keys->{$index} || {};
52             }
53              
54             # Clears the Hash of any existing data
55             sub reset {
56 13     13 1 25 my $self = shift;
57 13         26 %{$self->_Hash} = ();
  13         74  
58 13         25 %{$self->_all_level_keys} = ();
  13         58  
59 13         22 %{$self->_def_key_bitmasks} = ();
  13         44  
60 13         46 return $self;
61             }
62              
63 17     17 1 2196 sub clone { Clone::clone(shift) }
64              
65              
66             around BUILDARGS => sub {
67             my ($orig, $self, @args) = @_;
68             my %opt = (ref($args[0]) eq 'HASH') ? %{ $args[0] } : @args; # <-- arg as hash or hashref
69            
70             # Accept 'levels' as shorthand numeric value:
71             if($opt{levels} && looks_like_number $opt{levels}) {
72             my $num = $opt{levels} - 1;
73             $opt{delimiter} ||= '/';
74             my @levels = ({ delimiter => $opt{delimiter} }) x $num;
75             $opt{levels} = [ @levels, {} ];
76             delete $opt{delimiter};
77             }
78              
79             return $self->$orig(%opt);
80             };
81              
82              
83             sub BUILD {
84 12     12 0 342 my $self = shift;
85 12         50 $self->_post_validate;
86             }
87              
88             sub _post_validate {
89 12     12   23 my $self = shift;
90              
91 12 100       91 if($self->allow_deep_values) {
92 11         22 for my $Lvl (@{$self->levels}) {
  11         54  
93 76 50 66     653 die join("",
94             "Level delimiters must be different from the deep_delimiter ('",
95             $self->deep_delimiter,"').\n",
96             "Please specify a different level delimiter or change 'deep_delimiter'"
97             ) if ($Lvl->delimiter && $Lvl->delimiter eq $self->deep_delimiter);
98             }
99             }
100              
101             }
102              
103             sub coercer {
104 2     2 1 1094 my $self = (shift)->clone;
105 2     2   19 return sub { $self->coerce(@_) };
  2         9  
106             }
107              
108             sub coerce {
109 7     7 1 783 my ($self, @args) = @_;
110 7 50       58 die 'coerce() is not a class method' unless (blessed $self);
111 7 100       32 if(scalar(@args) == 1){
112 4 100       31 if(ref($args[0])) {
    50          
113 1 50 33     18 return $args[0] if (blessed($args[0]) && blessed($args[0]) eq __PACKAGE__);
114 0 0       0 @args = @{$args[0]} if (ref($args[0]) eq 'ARRAY');
  0         0  
115             }
116             elsif(! defined $args[0]) {
117 0         0 return $self->clone->reset;
118             }
119             }
120 6         27 my $new = $self->clone->reset;
121 6 50       54 return scalar(@args) > 0 ? $new->load(@args) : $new;
122             }
123              
124             sub lookup {
125 50     50 1 3629 my ($self, $key_str, @addl) = @_;
126 50 50       131 return undef unless (defined $key_str);
127 50 50       147 die join(' ',
128             "lookup() expects a single composite key string argument",
129             "(did you mean to use 'lookup_path'?)"
130             ) if (scalar(@addl) > 0);
131 50         163 return $self->lookup_path( $self->resolve_key_path($key_str) );
132             }
133              
134             sub lookup_path {
135 63     63 1 188 my ($self, @path) = @_;
136             # lookup_path() is the same as get_path() when lookup_mode is 'get':
137 63 100       1676 return $self->get_path(@path) if ($self->lookup_mode eq 'get');
138            
139 61 50       1986 return undef unless (defined $path[0]);
140            
141 61         83 my $hash_val;
142              
143             # If the exact path is set and is NOT a hash (that may need merging),
144             # return it outright:
145 61 100       151 if($self->exists_path(@path)) {
146 28         94 my $val = $self->get_path(@path);
147 28 100 66     570 return $val unless (
      100        
148             ref $val && ref($val) eq 'HASH'
149             && $self->lookup_mode eq 'merge'
150             );
151             # Set the first hash_val:
152 8 50 33     100 $hash_val = $val if(ref $val && ref($val) eq 'HASH');
153             }
154            
155 41         356 my @set = $self->_enumerate_default_paths(@path);
156            
157 41         71 my @values = ();
158 41         71 for my $dpath (@set) {
159 75 100       759 $self->exists_path(@$dpath) or next;
160 45         344 my $val = $self->get_path(@$dpath);
161 45 100       1107 return $val unless ($self->lookup_mode eq 'merge');
162 39 100 66     392 if (ref $val && ref($val) eq 'HASH') {
163             # Set/merge hashes:
164 23 100       93 $hash_val = $hash_val ? merge($val,$hash_val) : $val;
165             }
166             else {
167             # Return the first non-hash value unless a hash has already been
168             # encountered, and if that is the case, we can't merge a non-hash,
169             # return the hash we already had now
170 16 50       148 return $hash_val ? $hash_val : $val;
171             }
172             }
173            
174             # If nothing was found, $hash_val will still be undef:
175 19         497 return $hash_val;
176             }
177              
178             # Only returns the lookup_path value if it is a "leaf" -
179             # any value that is NOT a populated HashRef
180             sub lookup_leaf_path {
181 0     0 1 0 my ($self, @path) = @_;
182 0         0 my $v = $self->lookup_path(@path);
183 0 0 0     0 return (ref $v && ref($v) eq 'HASH' && scalar(keys %$v) > 0) ? undef : $v;
184             }
185              
186             sub get {
187 3     3 1 9 my ($self, $key_str, @addl) = @_;
188 3 50       13 return undef unless (defined $key_str);
189 3 50       13 die join(' ',
190             "get() expects a single composite key string argument",
191             "(did you mean to use 'get_path'?)"
192             ) if (scalar(@addl) > 0);
193 3         13 return $self->get_path( $self->resolve_key_path($key_str) );
194             }
195              
196             sub get_path {
197 82     82 1 260 my ($self, @path) = @_;
198 82 50       187 return undef unless (defined $path[0]);
199              
200 82         90 my $value;
201 82         162 my $ev_path = $self->_as_eval_path(@path);
202 82         5379 eval join('','$value = $self->Data->',$ev_path);
203            
204 82         688 return $value;
205             }
206              
207             sub exists {
208 0     0 1 0 my ($self, $key_str, @addl) = @_;
209 0 0       0 return undef unless (defined $key_str);
210 0 0       0 die join(' ',
211             "exists() expects a single composite key string argument",
212             "(did you mean to use 'exists_path'?)"
213             ) if (scalar(@addl) > 0);
214 0         0 return $self->exists_path( $self->resolve_key_path($key_str) );
215             }
216              
217             sub exists_path {
218 142     142 1 389 my ($self, @path) = @_;
219 142 50       294 return 0 unless (defined $path[0]);
220              
221 142         309 my $ev_path = $self->_as_eval_path(@path);
222 142         9916 return eval join('','exists $self->Data->',$ev_path);
223             }
224              
225             sub delete {
226 4     4 1 9 my ($self, $key_str, @addl) = @_;
227 4 50       14 return undef unless (defined $key_str);
228 4 50       11 die join(' ',
229             "delete() expects a single composite key string argument",
230             "(did you mean to use 'delete_path'?)"
231             ) if (scalar(@addl) > 0);
232 4         12 return $self->delete_path( $self->resolve_key_path($key_str) );
233             }
234              
235             sub delete_path {
236 6     6 1 16 my ($self, @path) = @_;
237 6 50       15 return 0 unless (defined $path[0]);
238            
239             # TODO: should this die?
240 6 100       19 return undef unless ($self->exists_path(@path));
241            
242 4         28 my $data = $self->Data; #<-- this is a *copy* of the data
243 4         15 my $ev_path = $self->_as_eval_path(@path);
244            
245             # Delete teh value from our copy:
246 4         7 my $ret; eval join('','$ret = delete $data->',$ev_path);
  4         273  
247            
248             # To delete safely, we actually have to reload all the data
249             # (except what is being deleted), from scratch. This is to
250             # make sure all the other indexes and counters remain in a
251             # consistent state:
252 4         22 $self->reset->load($data);
253            
254             # Return whatever was actually returned from the "real" delete:
255 4         29 return $ret;
256             }
257              
258             # Use bitwise math to enumerate all possible prefix, default key paths:
259             sub _enumerate_default_paths {
260 41     41   120 my ($self, @path) = @_;
261              
262 41         103 my $def_val = $self->default_key;
263 41         108 my $depth = $self->num_levels;
264              
265 41         79 my @set = ();
266 41         62 my %seen_combo = ();
267              
268             ## enumerate every possible default path bitmask (slow with many levels):
269             #my $bits = 2**$depth;
270             #my @mask_sets = ();
271             #push @mask_sets, $bits while(--$bits >= 0);
272            
273             # default path bitmasks only for paths we know are set (much faster):
274 41         44 my @mask_sets = keys %{$self->_def_key_bitmasks};
  41         184  
275            
276             # Re-sort the mask sets as reversed *strings*, because we want
277             # '011' to come before '110'
278 94         436 @mask_sets = sort {
279 41         176 reverse(sprintf('%0'.$depth.'b',$a)) cmp
280             reverse(sprintf('%0'.$depth.'b',$b))
281             } @mask_sets;
282            
283 41         68 for my $mask (@mask_sets) {
284 107         149 my @combo = ();
285 107         178 my $check_mask = 2**$depth >> 1;
286 107         149 for my $k (@path) {
287             # Use bitwise AND to decide whether or not to swap the
288             # default value for the actual key:
289 514 100       980 push @combo, $check_mask & $mask ? $def_val : $k;
290            
291             # Shift the check bit position by one for the next key:
292 514         705 $check_mask = $check_mask >> 1;
293             }
294 107 100       538 push @set, \@combo unless ($seen_combo{join('/',@combo)}++);
295             }
296              
297 41         239 return @set;
298             }
299              
300             sub load {
301 26     26 1 1065 my $self = shift;
302 26         152 return $self->_load(0,$self->_Hash,@_);
303             }
304              
305             sub _load {
306 371     371   771 my ($self, $index, $noderef, @args) = @_;
307            
308 371 50       1199 my $Lvl = $self->levels->[$index] or die "Bad level index '$index'";
309 371         851 my $last_level = ! $self->levels->[$index+1];
310            
311 371         681 for my $arg (@args) {
312 394 50       719 die "Undef keys are not allowed" unless (defined $arg);
313            
314 394   100     1503 my $force_composite = $self->{_force_composite} || 0;
315 394         886 local $self->{_force_composite} = 0; #<-- clear if set to prevetn deep recursion
316 394 100       783 unless (ref $arg) {
317             # hanging string/scalar, convert using default value
318 18         73 $arg = { $arg => $self->default_value };
319 18         32 $force_composite = 1;
320             }
321            
322 394 50       831 die "Cannot load non-hash reference!" unless (ref($arg) eq 'HASH');
323            
324 394         999 for my $key (keys %$arg) {
325 412 50 33     1690 die "Only scalar/string keys are allowed"
326             unless (defined $key && ! ref($key));
327            
328 412         611 my $val = $arg->{$key};
329 412   66     1442 my $is_hashval = ref $val && ref($val) eq 'HASH';
330            
331 412 100 100     1255 if( $force_composite || $self->_is_composite_key($key,$index) ) {
332 56         101 my $no_fill = $is_hashval;
333 56         147 my @path = $self->resolve_key_path($key,$index,$no_fill);
334 56         117 my $lkey = pop @path;
335 56         97 my $hval = {};
336 56 50       129 if(scalar(@path) > 0) {
337 56         179 $self->_init_hash_path($hval,@path)->{$lkey} = $val;
338             }
339             else {
340 0         0 $hval->{$lkey} = $val;
341             }
342 56         175 $self->_load($index,$noderef,$hval);
343             }
344             else {
345            
346 356         940 local $self->{_path_bitmask} = $self->{_path_bitmask};
347 356   100     424 my $bm = 0; $self->{_path_bitmask} ||= \$bm;
  356         854  
348 356         480 my $bmref = $self->{_path_bitmask};
349 356 100       959 if($key eq $self->default_key) {
350 222         432 my $depth = 2**($self->num_levels);
351 222         549 $$bmref = $$bmref | ($depth >> $index+1);
352             }
353            
354 356         1126 $self->_all_level_keys->{$index}{$key} = 1;
355 356 100       601 if($is_hashval) {
356 304         735 $self->_init_hash_path($noderef,$key);
357 304 100       564 if($last_level) {
358 15         62 $noderef->{$key} = merge($noderef->{$key}, $val);
359             }
360             else {
361             # Set via recursive:
362 289         1142 $self->_load($index+1,$noderef->{$key},$val);
363             }
364             }
365             else {
366 52         104 $noderef->{$key} = $val;
367             }
368            
369 356 100 100     2342 if($index == 0 && $$bmref) {
370 42         321 $self->_def_key_bitmasks->{$$bmref} = 1;
371             }
372             }
373             }
374             }
375            
376 371         1158 return $self;
377             }
378              
379              
380             sub path_to_composite_key {
381 3     3 1 12 my ($self, @path) = @_;
382 3         94 return $self->_path_to_composite_key(0,@path);
383             }
384              
385             sub _path_to_composite_key {
386 9     9   22 my ($self, $index, @path) = @_;
387              
388 9 50       29 my $Lvl = $self->levels->[$index] or die "Bad level index '$index'";
389 9         21 my $last_level = ! $self->levels->[$index+1];
390            
391 9 100       17 if($last_level) {
392 3   50     11 my $del = $self->deep_delimiter || '.';
393 3         32 return join($del,@path);
394             }
395             else {
396 6         8 my $key = shift @path;
397 6 50       31 return scalar(@path) > 0 ? join(
398             $Lvl->delimiter,$key,
399             $self->_path_to_composite_key($index+1,@path)
400             ) : join('',$key,$Lvl->delimiter);
401             }
402             }
403              
404              
405             sub _init_hash_path {
406 360     360   854 my ($self,$hash,@path) = @_;
407 360 50 33     1635 die "Not a hash" unless (ref $hash && ref($hash) eq 'HASH');
408 360 50       699 die "No path supplied" unless (scalar(@path) > 0);
409            
410 360         765 my $ev_path = $self->_as_eval_path( @path );
411            
412 360         514 my $hval;
413 360         23438 eval join('','$hash->',$ev_path,' ||= {}');
414 360         18910 eval join('','$hval = $hash->',$ev_path);
415 360 50 33     2688 eval join('','$hash->',$ev_path,' = {}') unless (
416             ref $hval && ref($hval) eq 'HASH'
417             );
418            
419 360         929 return $hval;
420             }
421              
422              
423             sub set {
424 3     3 1 14 my ($self,$key,$value) = @_;
425 3 50       45 die "bad number of arguments passed to set" unless (scalar(@_) == 3);
426 3 50 33     33 die '$key value is required' unless ($key && $key ne '');
427 3         14 local $self->{_force_composite} = 1;
428 3         25 $self->load({ $key => $value });
429             }
430              
431              
432             sub _as_eval_path {
433 588     588   1156 my ($self,@path) = @_;
434 1710         4877 return (scalar(@path) > 0) ? join('',
435 588 50       1300 map { '{"'.$_.'"}' } @path
436             ) : undef;
437             }
438              
439             sub _eval_key_path {
440 0     0   0 my ($self, $key, $index) = @_;
441 0         0 return $self->_as_eval_path(
442             $self->resolve_key_path($key,$index)
443             );
444             }
445              
446             # recursively scans the supplied key for any special delimiters defined
447             # by any of the levels, or the deep delimiter, if deep values are enabled
448             sub _is_composite_key {
449 3308     3308   4444 my ($self, $key, $index) = @_;
450 3308   100     5624 $index ||= 0;
451            
452 3308         5276 my $Lvl = $self->levels->[$index];
453              
454 3308 100       5196 if ($Lvl) {
455 2955 100 100     8900 return 0 if ($Lvl->registered_keys && $Lvl->registered_keys->{$key});
456 2950   100     6863 return $Lvl->_peel_str_key($key) || $self->_is_composite_key($key,$index+1);
457             }
458             else {
459 353 50       813 if($self->allow_deep_values) {
460 353         593 my $del = $self->deep_delimiter;
461 353         5618 return $key =~ /\Q${del}\E/;
462             }
463             else {
464 0         0 return 0;
465             }
466             }
467             }
468              
469             sub resolve_key_path {
470 950     950 1 5229 my ($self, $key, $index, $no_fill) = @_;
471 950   100     2351 $index ||= 0;
472 950   100     3696 $no_fill ||= $self->no_fill;
473            
474 950         1723 my $Lvl = $self->levels->[$index];
475 950         2239 my $last_level = ! $self->levels->[$index+1];
476            
477 950 100       1641 if ($Lvl) {
478 817         2160 my ($peeled,$leftover) = $Lvl->_peel_str_key($key);
479 817 100       1565 if($peeled) {
480 210         654 local $self->{_composite_key_peeled} = 1;
481             # If a key was peeled, move on to the next level with leftovers:
482 210 100       750 return ($peeled, $self->resolve_key_path($leftover,$index+1,$no_fill)) if ($leftover);
483            
484             # If there were no leftovers, recurse again only for the last level,
485             # otherwise, return now (this only makes a difference for deep values)
486 18 50       136 return $last_level ? $self->resolve_key_path($peeled,$index+1,$no_fill) : $peeled;
487             }
488             else {
489             # If a key was not peeled, add the default key at the top of the path
490             # only if we're not already at the last level and 'no_fill' is not set
491             # (and we've already peeled at least one key)
492 607         1765 my @path = $self->resolve_key_path($key,$index+1,$no_fill);
493 607   66     2231 my $as_is = $last_level || ($no_fill && $self->{_composite_key_peeled});
494 607 100 100     5742 return $self->no_pad || $as_is ? @path : ($self->default_key,@path);
495             }
496             }
497             else {
498 133 100       407 if($self->allow_deep_values) {
499 132         448 my $del = $self->deep_delimiter;
500 132         753 return split(/\Q${del}\E/,$key);
501             }
502             else {
503 1         5 return $key;
504             }
505             }
506             }
507              
508              
509             sub _coerce_levels_param {
510 12     12   168 my $val = shift;
511 12 50 33     191 return $val unless (ref($val) && ref($val) eq 'ARRAY');
512            
513 12         35 my %seen = ();
514 12         24 my $i = 0;
515 12         28 my @new = ();
516 12         32 for my $itm (@$val) {
517 79 50       2486 return $val if (blessed $itm);
518            
519 79 50 33     410 die "'levels' must be an arrayref of hashrefs" unless (
520             ref($itm) && ref($itm) eq 'HASH'
521             );
522            
523 79 50 66     282 die "duplicate level name '$itm->{name}'" if (
524             $itm->{name} && $seen{$itm->{name}}++
525             );
526            
527 79 50 66     333 die "the last level is not allowed to have a delimiter" if(
528             scalar(@$val) == ++$i
529             && $itm->{delimiter}
530             );
531            
532 79         2017 push @new, Hash::Layout::Level->new({
533             %$itm,
534             index => $i-1
535             });
536             }
537            
538 12 50       338 die "no levels specified" unless (scalar(@new) > 0);
539            
540 12         442 return \@new;
541             }
542              
543              
544             # debug method:
545             sub def_key_bitmask_strings {
546 0     0 1   my $self = shift;
547 0           my $depth = $self->num_levels;
548 0           my @masks = keys %{$self->_def_key_bitmasks};
  0            
549 0           map { sprintf('%0'.$depth.'b',$_) } @masks;
  0            
550             }
551              
552             1;
553              
554              
555             __END__