File Coverage

blib/lib/Hash/Union.pm
Criterion Covered Total %
statement 84 107 78.5
branch 55 100 55.0
condition 9 19 47.3
subroutine 9 9 100.0
pod 1 1 100.0
total 158 236 66.9


line stmt bran cond sub pod time code
1             package Hash::Union;
2              
3 5     5   90838 use warnings FATAL => 'all';
  5         11  
  5         273  
4 5     5   30 use strict;
  5         10  
  5         327  
5              
6             =head1 NAME
7              
8             Hash::Union - smart hashes merging
9              
10             =head1 VERSION
11              
12             Version 0.03
13              
14             =cut
15              
16             our $VERSION = '0.03';
17              
18             =head1 SYNOPSIS
19              
20             use Hash::Union 'union';
21             use Data::Dumper; # for debug only
22              
23             my $config_base = { # default application config
24             'database' => 'production', # production database
25             'html_dirs' => [ # search paths for html documents
26             '/docs/html/main',
27             '/docs/html/default'
28             ],
29             'text_dirs' => [ # search paths fo text documents
30             '/docs/text/main',
31             '/docs/text/default'
32             ]
33             };
34              
35             my $config_local = { # locally customized config
36             'database' => 'stageing', # devel database
37             'prepend: html_dirs' => [ # local html pages preferred
38             '/local/html/main',
39             '/local/html/default'
40             ],
41             'append: text_dirs' => [ # fallback for nonexistent text
42             '/local/text/main',
43             '/local/text/default'
44             ]
45             };
46              
47             # now merge default with local
48             my $config = union( [ $config_base, $config_local ] );
49              
50             print Dumper $config;
51              
52             ========
53              
54             $VAR1 = {
55             'database' => 'stageing',
56             'html_dirs' => [
57             '/local/html/main',
58             '/local/html/default',
59             '/docs/html/main',
60             '/docs/html/default'
61             ],
62             'text_dirs' => [
63             '/docs/text/main',
64             '/docs/text/default',
65             '/local/text/main',
66             '/local/text/default'
67             ]
68             };
69              
70             =cut
71              
72 5     5   30 use base qw'Exporter';
  5         13  
  5         478  
73 5     5   26 use vars qw'@EXPORT_OK';
  5         12  
  5         294  
74              
75              
76             =head1 EXPORT_OK
77              
78             =head2 union( \@hash_references, %options );
79              
80             Supported options:
81              
82             =over
83              
84             =item * reverse
85              
86             Merge all hash references in reverse order.
87              
88             =item * simple
89              
90             Don't apply complex merging logic (ignore keys special meaning).
91              
92             =back
93              
94             =cut
95              
96             @EXPORT_OK = qw'&union';
97              
98 5     5   38 use Carp qw'croak';
  5         3334  
  5         2472  
99 5     5   7260 use Storable qw'dclone';
  5         18583  
  5         430  
100              
101             use constant {
102 5         8743 OP_SET => 0,
103             OP_SETIF => 1,
104             OP_PREPEND => 2,
105             OP_APPEND => 3,
106 5     5   46 };
  5         9  
107              
108              
109             sub union {
110 4     4 1 121 my ($hashes,%opts) = @_;
111              
112 4 50       22 croak "error: arrayref required" unless ref $hashes eq 'ARRAY';
113              
114             # never modify source hashes keys nor values
115 4         536 $hashes = dclone $hashes;
116              
117             # exotic option
118 4 50       20 @$hashes = reverse @$hashes if $opts{reverse};
119              
120 4         14 my $left = shift @$hashes;
121 4 50 33     34 croak "error: hashref required" unless ref $left eq 'HASH' || !defined $left;
122              
123 4         16 while (@$hashes) {
124 4         16 my $right = shift @$hashes;
125 4 50 33     21 croak "error: hashref required" unless ref $right eq 'HASH' || !defined $right;
126 4         21 $left = _union($left,$right,%opts);
127             }
128 4         19 return $left;
129             }
130              
131             # internal routine
132             sub _union {
133 8     8   17 my ($l,$r,%opts) = @_;
134              
135             # undef handling
136 8   50     22 $l ||= {};
137 8   50     23 $r ||= {};
138              
139             # normalize left keys
140 8 50       21 unless ($opts{simple}) {
141 8         26 for (keys %$l) {
142 29 100       178 if (/^(?:\?=|ifnone:)\s*(.*)/) { # '?= key', 'ifnone: key'
    50          
    50          
    100          
143 2 50       8 croak "left '$_' violates with '$1'" if exists $l->{$1};
144 2         9 $l->{$1} = delete $l->{$_};
145             } elsif (/^(?:\+=|prepend:)\s*(.*)/) { # '+= key', prepend: key'
146 0 0       0 croak "left '$_' violates with '$1'" if exists $l->{$1};
147 0         0 $l->{$1} = delete $l->{$_};
148             } elsif (/^(?:=\+|append:)\s*(.*)/) { # '=+ key', 'append: key'
149 0 0       0 croak "left '$_' violates with '$1'" if exists $l->{$1};
150 0         0 $l->{$1} = delete $l->{$_};
151             } elsif (/^(?:=|set:)\s*(.*)/) { # '= key', 'set: key'
152 2 50       9 croak "left '$_' violates with '$1'" if exists $l->{$1};
153 2         9 $l->{$1} = delete $l->{$_};
154             }
155             }
156             }
157              
158             # now right...
159 8         29 for my $k (keys %$r) {
160 28         48 my ($lk, $op) = ($k, OP_SET);
161              
162 28 50       79 unless ($opts{simple}) {
163 28 100       160 if ($k=~/^(?:\?=|ifnone:)\s*(.*)/) { # '?= key', 'ifnone: key'
    100          
    100          
    100          
164 4 50       19 croak "right '$_' violates with '$1'" if exists $r->{$1};
165 4         11 ($lk, $op) = ($1, OP_SETIF);
166             } elsif ($k=~/^(?:\+=|prepend:)\s*(.*)/) { # '+= key', 'prepend: key'
167 8 50       23 croak "right '$_' violates with '$1'" if exists $r->{$1};
168 8         17 ($lk, $op) = ($1, OP_PREPEND);
169             } elsif ($k=~/^(?:=\+|append:)\s*(.*)/) { # '=+ key', 'append: key'
170 8 50       27 croak "right '$_' violates with '$1'" if exists $r->{$1};
171 8         21 ($lk, $op) = ($1, OP_APPEND);
172             } elsif ($k=~/^(?:=|set:)\s*(.*)/) { # '= key', 'set: key'
173 3 50       10 croak "right '$_' violates with '$1'" if exists $r->{$1};
174 3         8 ($lk, $op) = ($1, OP_SET);
175             }
176             }
177              
178             # undefs cases
179 28 50       64 next unless defined $r->{$k};
180 28 100       60 unless (defined $l->{$lk}) {
181 3         9 $l->{$lk} = $r->{$k};
182 3         11 next;
183             }
184              
185             # res vs !ref
186 25 50 66     568 croak "left '$lk' is ref, right '$k' isn't" if ref $l->{$lk} && !ref $r->{$k};
187 25 50 66     976 croak "left '$lk' isn't ref, right '$k' is" if !ref $l->{$lk} && ref $r->{$k};
188              
189             # scalars
190 25 100       480 unless (ref $l->{$lk}) {
191 13 100       41 if ($op==OP_SET) {
    100          
    100          
    50          
192 7         15 $l->{$lk} = $r->{$k};
193             } elsif ($op==OP_SETIF) {
194 2   33     7 $l->{$lk} ||= $r->{$k};
195             } elsif ($op==OP_PREPEND) {
196 2         7 $l->{$lk} = $r->{$k}.$l->{$lk};
197             } elsif ($op==OP_APPEND) {
198 2         8 $l->{$lk} .= $r->{$k};
199             }
200 13         32 next;
201             }
202              
203             # incompatible kind of refs
204 12 50       35 croak "type of left '$lk' incompatible with type of right '$k'" if ref $l->{$lk} ne ref $r->{$k};
205              
206             # scalars
207 12 50       27 if (ref $l->{$lk} eq 'SCALAR') {
208 0 0       0 if ($op==OP_SET) {
    0          
    0          
    0          
209 0         0 $l->{$lk} = $r->{$k};
210             } elsif ($op==OP_SETIF) {
211 0 0       0 $l->{$lk} = $r->{$k} unless ${$l->{$lk}};
  0         0  
212             } elsif ($op==OP_PREPEND) {
213 0         0 ${$l->{$lk}} = ${$r->{$k}}.${$l->{$lk}};
  0         0  
  0         0  
  0         0  
214             } elsif ($op==OP_APPEND) {
215 0         0 ${$l->{$lk}} .= ${$r->{$k}};
  0         0  
  0         0  
216             }
217 0         0 next;
218             }
219              
220             # arrays
221 12 100       30 if (ref $l->{$lk} eq 'ARRAY') {
222 8 50       34 if ($op==OP_SET) {
    50          
    100          
    50          
223 0         0 $l->{$lk} = $r->{$k};
224             } elsif ($op==OP_SETIF) {
225 0 0       0 $l->{$lk} = $r->{$k} unless @{$l->{$lk}};
  0         0  
226             } elsif ($op==OP_PREPEND) {
227 4         15 unshift @{$l->{$lk}}, @{$r->{$k}};
  4         6  
  4         9  
228             } elsif ($op==OP_APPEND) {
229 4         5 push @{$l->{$lk}}, @{$r->{$k}};
  4         8  
  4         7  
230             }
231 8         37 next;
232             }
233              
234             # hashes
235 4 50       9 if (ref $l->{$lk} eq 'HASH') {
236 4 50       32 if ($op==OP_SET) {
    50          
    100          
    50          
237 0         0 $l->{$lk} = _union($l->{$lk}, $r->{$k},%opts);
238             } elsif ($op==OP_SETIF) {
239 0 0       0 $l->{$lk} = _union($l->{$lk}, $r->{$k}, %opts) unless %{$l->{$lk}};
  0         0  
240             } elsif ($op==OP_PREPEND) {
241 2         11 $l->{$lk} = _union($r->{$k}, $l->{$lk}, %opts);
242             } elsif ($op==OP_APPEND) {
243 2         11 $l->{$lk} = _union($l->{$lk}, $r->{$k}, %opts);
244             }
245 4         9 next;
246             }
247              
248             # wtf?
249 0         0 croak "unknown type of left '$lk'";
250             }
251              
252 8         69 return $l;
253             }
254              
255             1;
256              
257             __END__