File Coverage

lib/Hash/Flatten.pm
Criterion Covered Total %
statement 150 155 96.7
branch 84 96 87.5
condition 7 10 70.0
subroutine 20 21 95.2
pod 3 5 60.0
total 264 287 91.9


line stmt bran cond sub pod time code
1             ###############################################################################
2             # Purpose : Flatten/Unflatten nested data structures to/from key-value form
3             # Author : John Alden
4             # Created : Feb 2002
5             # CVS : $Id: Flatten.pm,v 1.19 2009/05/09 12:42:02 jamiel Exp $
6             ###############################################################################
7              
8             package Hash::Flatten;
9              
10 2     2   3364 use strict;
  2         5  
  2         87  
11 2     2   12 use Exporter;
  2         4  
  2         83  
12 2     2   30 use Carp;
  2         6  
  2         176  
13              
14 2     2   12 use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS $VERSION);
  2         4  
  2         374  
15             @ISA = qw(Exporter);
16             @EXPORT_OK = qw(flatten unflatten);
17             %EXPORT_TAGS = ('all' => \@EXPORT_OK);
18             $VERSION = ('$Revision: 1.19 $' =~ /([\d\.]+)/)[0];
19              
20 2     2   14 use constant DEFAULT_HASH_DELIM => '.';
  2         2  
  2         146  
21 2     2   10 use constant DEFAULT_ARRAY_DELIM => ':';
  2         3  
  2         116  
22              
23             #Check if we need to support overloaded stringification
24 2         11 use constant HAVE_OVERLOAD => eval {
25 2         4316 require overload;
26 2     2   17 };
  2         3  
27              
28             sub new
29             {
30 38     38 1 1221 my ($class, $options) = @_;
31 38 100       112 $options = {} unless ref $options eq 'HASH';
32 38         94 my $self = {
33             %$options
34             };
35            
36             #Defaults
37 38   100     190 $self->{HashDelimiter} ||= DEFAULT_HASH_DELIM;
38 38   100     158 $self->{ArrayDelimiter} ||= DEFAULT_ARRAY_DELIM;
39 38 100 66     136 $self->{EscapeSequence} = "\\" unless(defined $self->{EscapeSequence} && length($self->{EscapeSequence}) > 0);
40 38 100       105 $self->{EscapeSequence} = undef if($self->{DisableEscapes});
41            
42             #Sanity check: delimiters don't contain escape sequence
43 38 100       433 croak("Hash delimiter cannot contain escape sequence") if($self->{HashDelimiter} =~ /\Q$self->{EscapeSequence}\E/);
44 37 100       266 croak("Array delimiter cannot contain escape sequence") if($self->{ArrayDelimiter} =~ /\Q$self->{EscapeSequence}\E/);
45            
46 36         129 TRACE(__PACKAGE__." constructor - $self");
47 36         177 return bless($self, $class);
48             }
49              
50             sub flatten
51             {
52             #Convert functional to OO with default ctor
53 48 100   48 1 41362 if(ref $_[0] ne __PACKAGE__) {
54 24         88 return __PACKAGE__->new($_[1])->flatten($_[0]);
55             }
56              
57 24         580 my ($self, $hashref) = @_;
58 24 100       84 die("1st arg must be a hashref") unless(UNIVERSAL::isa($hashref, 'HASH'));
59            
60 23         86 my $delim = {
61             'HASH' => $self->{HashDelimiter},
62             'ARRAY' => $self->{ArrayDelimiter}
63             };
64 23         46 $self->{RECURSE_CHECK} = {};
65 23         64 my @flat = $self->_flatten_hash_level($hashref,$delim);
66 17         33 my %flat_hash = map {$_->[0], $_->[1]} @flat;
  66         189  
67 17         109 return \%flat_hash;
68             }
69              
70             sub unflatten
71             {
72             #Convert functional to OO with default ctor
73 26 100   26 1 35335 if(ref $_[0] ne __PACKAGE__) {
74 13         52 return __PACKAGE__->new($_[1])->unflatten($_[0]);
75             }
76            
77 13         22 my ($self, $hashref) = @_;
78 13 100       48 die("1st arg must be a hashref") unless(UNIVERSAL::isa($hashref, 'HASH'));
79              
80 12         44 my $delim = {
81             'HASH' => $self->{HashDelimiter},
82             'ARRAY' => $self->{ArrayDelimiter}
83             };
84            
85 12         38 my $regexp = '((?:' . quotemeta($delim->{'HASH'}) . ')|(?:' . quotemeta($delim->{'ARRAY'}) . '))';
86 12 100       30 if($self->{EscapeSequence}) {
87 11         26 $regexp = '(?{EscapeSequence}).')'.$regexp; #Use negative look behind
88             }
89 12         35 TRACE("regex = /$regexp/");
90            
91 12         17 my %expanded;
92 12         37 foreach my $key (keys %$hashref)
93             {
94 58         378 my $value = $hashref->{$key};
95 58         760 my @levels = split(/$regexp/, $key);
96            
97 58 100       221 my $finalkey = $self->_unescape((scalar(@levels) % 2 ? pop(@levels) : ''), $self->{EscapeSequence});
98 58         87 my $ptr = \%expanded;
99 58         131 while (@levels >= 2)
100             {
101 67         154 my $key = $self->_unescape(shift(@levels), $self->{EscapeSequence});
102 67         101 my $type = shift(@levels);
103 67 100       176 if ($type eq $delim->{'HASH'})
    50          
104             {
105 29 100       72 if (UNIVERSAL::isa($ptr, 'HASH')) {
106 25 100       67 $ptr->{$key} = {} unless exists $ptr->{$key};
107 25         72 $ptr = $ptr->{$key};
108             } else {
109 4 50       49 $ptr->[$key] = {} unless defined $ptr->[$key];
110 4         13 $ptr = $ptr->[$key];
111             }
112             }
113             elsif ($type eq $delim->{'ARRAY'})
114             {
115 38 100       101 if (UNIVERSAL::isa($ptr, 'HASH')) {
116 29 100       80 $ptr->{$key} = [] unless exists $ptr->{$key};
117 29         84 $ptr = $ptr->{$key};
118             } else {
119 9 100       25 $ptr->[$key] = [] unless defined $ptr->[$key];
120 9         26 $ptr = $ptr->[$key];
121             }
122             }
123             else
124             {
125 0         0 die "Type '$type' was not recognized. This should not happen.";
126             }
127             }
128              
129 58 100       139 if (UNIVERSAL::isa($ptr, 'HASH')) {
130 33         92 $ptr->{$finalkey} = $value;
131             } else {
132 25         69 $ptr->[$finalkey] = $value;
133             }
134             }
135 12         64 return \%expanded;
136             }
137              
138             #
139             # Private subroutines
140             #
141              
142             sub _flatten
143             {
144 113     113   172 my($self, $flatkey, $v, $delim) = @_;
145              
146 113         392 TRACE("flatten: $self - " . ref($v));
147              
148 113 100       341 if(UNIVERSAL::isa($v, 'REF'))
149             {
150 7         16 $v = $self->_follow_refs($v);
151             }
152              
153 112 100       625 if(UNIVERSAL::isa($v, 'HASH'))
    100          
    100          
    100          
154             {
155 26         67 return $self->_flatten_hash_level($v, $delim, $flatkey);
156             }
157             elsif(UNIVERSAL::isa($v, 'ARRAY'))
158             {
159 14         40 return $self->_flatten_array_level($v, $delim, $flatkey);
160             }
161             elsif(UNIVERSAL::isa($v, 'GLOB'))
162             {
163 3         8 $v = $self->_flatten_glob_ref($v);
164             }
165             elsif(UNIVERSAL::isa($v, 'SCALAR'))
166             {
167 7         19 $v = $self->_flatten_scalar_ref($v);
168             }
169 70         266 return [$flatkey, $v];
170             }
171              
172             sub _follow_refs
173             {
174 7     7   12 my ($self, $rscalar) = @_;
175 7         21 while (UNIVERSAL::isa($rscalar, 'REF'))
176             {
177 14 50       28 if ($self->{RECURSE_CHECK}{_stringify_ref($rscalar)}++)
178             {
179 0         0 die "Recursive data structure detected. Cannot flatten recursive structures.";
180             }
181              
182 14 100       34 if(defined $self->{OnRefRef}) {
183 6 100       24 if(ref $self->{OnRefRef} eq 'CODE') {
    100          
    50          
184 4         9 TRACE("Executing coderef");
185 4         10 $rscalar = $self->{OnRefRef}->($rscalar);
186 4         23 next;
187             } elsif($self->{OnRefRef} eq 'warn') {
188 1         8 warn("$rscalar is a ".(ref $rscalar)." and will be followed");
189             } elsif($self->{OnRefRef} eq 'die') {
190 1         13 die("$rscalar is a ".(ref $rscalar));
191             }
192             }
193 9         28 $rscalar = $$rscalar;
194             }
195 6         12 return $rscalar;
196             }
197              
198             sub _flatten_hash_level
199             {
200 49     49   76 my ($self, $hashref, $delim, $prefix) = @_;
201 49         81 TRACE("_flatten_hash_level called");
202            
203 49 100       1340 if ($self->{RECURSE_CHECK}{_stringify_ref($hashref)}++)
204             {
205 2         33 die "Recursive data structure detected at this point in the structure: '$prefix'. Cannot flatten recursive structures.";
206             }
207              
208 47         145 my @flat;
209 47         150 for my $k (keys %$hashref)
210             {
211 79         235 TRACE("_flatten_hash_level: flattening: $k");
212 79         124 my $v = $hashref->{$k};
213 79         284 $k = $self->_escape($k, $self->{EscapeSequence}, [values %$delim]);
214 79 100       226 my $flatkey = (defined($prefix) ? $prefix.$delim->{'HASH'}.$k : $k);
215 79         189 push @flat, $self->_flatten($flatkey, $v, $delim);
216             }
217 38         141 return @flat;
218             }
219              
220             sub _flatten_array_level
221             {
222 14     14   25 my ($self, $arrayref, $delim, $prefix) = @_;
223              
224 14 100       29 if ($self->{RECURSE_CHECK}{_stringify_ref($arrayref)}++)
225             {
226 1         14 die "Recursive data structure detected at this point in the structure: '$prefix'. Cannot flatten recursive structures.";
227             }
228              
229 13         19 my @flat;
230 13         30 foreach my $ind (0 .. $#$arrayref)
231             {
232 34 50       97 my $flatkey = (defined($prefix) ? $prefix.$delim->{'ARRAY'}.$ind : $ind);
233 34         40 my $v = $arrayref->[$ind];
234 34         69 push @flat, $self->_flatten($flatkey, $v, $delim);
235             }
236 12         54 return @flat;
237             }
238              
239             sub _flatten_scalar_ref
240             {
241 7     7   9 my ($self, $rscalar) = @_;
242 7 100       18 if(defined $self->{OnRefScalar}) {
243 2 100       13 if(ref $self->{OnRefScalar} eq 'CODE') {
    50          
    50          
244 1         3 TRACE("Executing coderef");
245 1         3 return $self->{OnRefScalar}->($rscalar);
246             } elsif($self->{OnRefScalar} eq 'warn') {
247 0         0 warn("$rscalar is a ".(ref $rscalar)." and will be followed");
248             } elsif($self->{OnRefScalar} eq 'die') {
249 1         14 die("$rscalar is a ".(ref $rscalar));
250             }
251             }
252 5         9 return $$rscalar;
253             }
254              
255             sub _flatten_glob_ref
256             {
257 3     3   6 my($self, $rglob) = @_;
258 3 100       10 if(defined $self->{OnRefGlob}) {
259 2 100       12 if(ref $self->{OnRefGlob} eq 'CODE') {
    50          
    50          
260 1         5 TRACE("Executing coderef");
261 1         4 return $self->{OnRefGlob}->($rglob);
262             } elsif($self->{OnRefGlob} eq 'warn') {
263 0         0 warn("$rglob is a ".(ref $rglob)." and will be followed");
264             } elsif($self->{OnRefGlob} eq 'die') {
265 1         12 die("$rglob is a ".(ref $rglob));
266             }
267             }
268 1         2 return $rglob;
269             }
270              
271             sub _escape
272             {
273 79     79   131 my ($self, $string, $eseq, $delim) = @_;
274 79 100       141 return $string unless($eseq); #no-op
275 76 50       202 $delim = [] unless(ref $delim eq 'ARRAY');
276            
277 76         118 foreach my $char($eseq, @$delim) {
278 228 50 33     857 next unless(defined $char && length($char));
279 228         2026 $string =~ s/\Q$char\E/$eseq$char/sg;
280             }
281            
282 76         171 return $string;
283             }
284              
285             sub _unescape
286             {
287 125     125   163 my ($self, $string, $eseq) = @_;
288 125 100       251 return $string unless($eseq); #no-op
289            
290             #Remove escape characters apart from double-escapes
291 119         330 $string =~ s/\Q$eseq\E(?!\Q$eseq\E)//gs;
292              
293             #Fold double-escapes down to single escapes
294 119         266 $string =~ s/\Q$eseq$eseq\E/$eseq/gs;
295              
296 119         257 return $string;
297             }
298              
299             sub _stringify_ref {
300 77     77   103 my $ref = shift;
301 77 50       163 return unless ref($ref); #Undef if not a reference
302 77 100       191 return overload::StrVal($ref) if(HAVE_OVERLOAD && overload::Overloaded($ref));
303 75         6554 return $ref.''; #Force type conversion here
304             }
305              
306             #Log::Trace stubs
307 295     295 0 332 sub TRACE {}
308 0     0 0   sub DUMP {}
309              
310             1;
311              
312             =head1 NAME
313              
314             Hash::Flatten - flatten/unflatten complex data hashes
315              
316             =head1 SYNOPSIS
317              
318             # Exported functions
319             use Hash::Flatten qw(:all);
320             $flat_hash = flatten($nested_hash);
321             $nested_hash = unflatten($flat_hash);
322            
323             # OO interface
324             my $o = new Hash::Flatten({
325             HashDelimiter => '->',
326             ArrayDelimiter => '=>',
327             OnRefScalar => 'warn',
328             });
329             $flat_hash = $o->flatten($nested_hash);
330             $nested_hash = $o->unflatten($flat_hash);
331              
332             =head1 DESCRIPTION
333              
334             Converts back and forth between a nested hash structure and a flat hash of delimited key-value pairs.
335             Useful for protocols that only support key-value pairs (such as CGI and DBMs).
336              
337             =head2 Functional interface
338              
339             =over 4
340              
341             =item $flat_hash = flatten($nested_hash, \%options)
342              
343             Reduces a nested data-structure to key-value form. The top-level container must be hashref. For example:
344              
345             $nested = {
346             'x' => 1,
347             'y' => {
348             'a' => 2,
349             'b' => 3
350             },
351             'z' => [
352             'a', 'b', 'c'
353             ]
354             }
355              
356             $flat = flatten($nested);
357             use Data::Dumper;
358             print Dumper($flat);
359              
360             $VAR1 = {
361             'y.a' => 2,
362             'x' => 1,
363             'y.b' => 3,
364             'z:0' => 'a',
365             'z:1' => 'b',
366             'z:2' => 'c'
367             };
368              
369             The C<\%options> hashref can be used to override the default behaviour (see L).
370              
371             =item $nested_hash = unflatten($flat_hash, \%options)
372              
373             The unflatten() routine takes the flattened hash and returns the original nested hash (see L though).
374              
375             =back
376              
377             =head2 OO interface
378              
379             =over 4
380              
381             =item $o = new Hash::Flatten(\%options)
382              
383             Options can be squirreled away in an object (see L)
384              
385             =item $flat = $o->flatten($nested)
386              
387             Flatten the structure using the options stored in the object.
388              
389             =item $nested = $o->unflatten($flat)
390              
391             Unflatten the structure using the options stored in the object.
392              
393             =back
394              
395             =head1 OPTIONS
396              
397             =over 4
398              
399             =item HashDelimiter and ArrayDelimiter
400              
401             By default, hash dereferences are denoted by a dot, and array dereferences are denoted by a colon. However
402             you may change these characters to any string you want, because you don't want there to be any confusion as to
403             which part of a string is the 'key' and which is the 'delimiter'. You may use multicharacter strings
404             if you prefer.
405              
406             =item OnRefScalar and OnRefRef and OnRefGlob
407              
408             Behaviour if a reference of this type is encountered during flattening.
409             Possible values are 'die', 'warn' (default behaviour but warns) or a coderef
410             which is passed the reference and should return the flattened value.
411              
412             By default references to references, and references to scalars, are followed silently.
413              
414             =item EscapeSequence
415              
416             This is the character or sequence of characters that will be used to escape the hash and array delimiters.
417             The default escape sequence is '\\'. The escaping strategy is to place the escape sequence in front of
418             delimiter sequences; the escape sequence itself is escaped by replacing it with two instances.
419              
420             =item DisableEscapes
421              
422             Stop the escaping from happening. No escape sequences will be added to flattened output, nor interpreted on the way back.
423              
424             B If your structure has keys that contain the delimiter characters, it will not be possible to unflatten the
425             structure correctly.
426              
427             =back
428              
429             =head1 CAVEATS
430              
431             Any blessings will be discarded during flattening, so that if you flatten an object you must re-bless() it on unflattening.
432              
433             Note that there is no delimiter for scalar references, or references to references.
434             If your structure to be flattened contains scalar, or reference, references these will be followed by default, i.e.
435             C<'foo' =E \\\\\\$foo>
436             will be collapsed to
437             C<'foo' =E $foo>.
438             You can override this behaviour using the OnRefScalar and OnRefRef constructor option.
439              
440             Recursive structures are detected and cause a fatal error.
441              
442             =head1 SEE ALSO
443              
444             The perlmonks site has a helpful introduction to when and why you
445             might want to flatten a hash: http://www.perlmonks.org/index.pl?node_id=234186
446              
447             =over 4
448              
449             =item CGI::Expand
450              
451             Unflattens hashes using "." as a delimiter, similar to Template::Toolkit's behaviour.
452              
453             =item Tie::MultiDim
454              
455             This provides a tie interface to unflattening a data structure if you specify a "template" for the structure of the data.
456              
457             =item MLDBM
458              
459             This also provides a tie interface but reduces a nested structure to key-value form by serialising the values below the top level.
460              
461             =back
462              
463             =head1 VERSION
464              
465             $Id: Flatten.pm,v 1.19 2009/05/09 12:42:02 jamiel Exp $
466              
467             =head1 AUTHOR
468              
469             John Alden E P Kent Ecpan _at_ bbc _dot_ co _dot_ ukE
470              
471             =head1 COPYRIGHT
472              
473             (c) BBC 2005. This program is free software; you can redistribute it and/or
474             modify it under the GNU GPL.
475              
476             See the file COPYING in this distribution, or http://www.gnu.org/licenses/gpl.txt
477              
478             =cut