File Coverage

blib/lib/Hash/Layout.pm
Criterion Covered Total %
statement 251 282 89.0
branch 106 162 65.4
condition 56 86 65.1
subroutine 38 43 88.3
pod 22 23 95.6
total 473 596 79.3


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