File Coverage

blib/lib/Tie/ShadowHash.pm
Criterion Covered Total %
statement 92 99 92.9
branch 34 40 85.0
condition 9 12 75.0
subroutine 12 13 92.3
pod 1 3 33.3
total 148 167 88.6


line stmt bran cond sub pod time code
1             # Tie::ShadowHash -- Merge multiple data sources into a hash.
2             #
3             # Copyright 1999, 2002, 2010 by Russ Allbery
4             #
5             # This program is free software; you may redistribute it and/or modify it
6             # under the same terms as Perl itself.
7             #
8             # This module combines multiple sources of data into a single tied hash, so
9             # that they can all be queried simultaneously, the source of any given
10             # key-value pair irrelevant to the client script. Data sources are searched
11             # in the order that they're added to the shadow hash. Changes to the hashed
12             # data aren't propagated back to the actual data files; instead, they're saved
13             # within the tied hash and override any data obtained from the data sources.
14              
15             ##############################################################################
16             # Modules and declarations
17             ##############################################################################
18              
19             package Tie::ShadowHash;
20             require 5.006;
21              
22 1     1   1179 use strict;
  1         3  
  1         52  
23 1     1   5 use vars qw($VERSION);
  1         2  
  1         1187  
24              
25             $VERSION = '1.00';
26              
27             ##############################################################################
28             # Regular methods
29             ##############################################################################
30              
31             # This should pretty much never be called; tie calls TIEHASH.
32             sub new {
33 0     0 0 0 my $class = shift;
34 0         0 return $class->TIEHASH (@_);
35             }
36              
37             # Given a file name and optionally a split regex, builds a hash out of the
38             # contents of the file. If the split sub exists, use it to split each line
39             # into an array; if the array has two elements, those are taken as the key and
40             # value. If there are more, the value is an anonymous array containing
41             # everything but the first. If there's no split sub, take the entire line
42             # modulo the line terminator as the key and the value the number of times it
43             # occurs in the file.
44             sub text_source {
45 4     4 0 8 my ($self, $file, $split) = @_;
46 4 50       161 unless (open (HASH, '<', $file)) {
47 0         0 require Carp;
48 0         0 Carp::croak ("Can't open file $file: $!");
49             }
50 4         28 local $_;
51 4         6 my ($key, @rest, %hash);
52 4         66 while () {
53 48         58 chomp;
54 48 100       78 if (defined $split) {
55 6         16 ($key, @rest) = &$split ($_);
56 6 100       73 $hash{$key} = (@rest == 1) ? $rest[0] : [ @rest ];
57             } else {
58 42         138 $hash{$_}++;
59             }
60             }
61 4         44 close HASH;
62 4         16 return \%hash;
63             }
64              
65             # Add data sources to the shadow hash. This takes a list of either anonymous
66             # arrays (in which case the first element is the type of source and the rest
67             # are arguments), filenames (in which case it's taken to be a text file with
68             # each line being a key), or hash references (possibly to tied hashes).
69             sub add {
70 8     8 1 810 my ($self, @sources) = @_;
71 8         17 for my $source (@sources) {
72 9 100       29 if (ref $source eq 'ARRAY') {
    100          
73 2         5 my ($type, @args) = @$source;
74 2 50       5 if ($type eq 'text') {
75 2         6 $source = $self->text_source (@args);
76             } else {
77 0         0 require Carp;
78 0         0 Carp::croak ("Invalid source type $type");
79             }
80             } elsif (!ref $source) {
81 2         6 $source = $self->text_source ($source);
82             }
83 9         11 push (@{ $$self{SOURCES} }, $source);
  9         38  
84             }
85 8         30 return 1;
86             }
87              
88             ##############################################################################
89             # Tie methods
90             ##############################################################################
91              
92             # DELETED is a hash holding all keys that have been deleted; it's checked
93             # first on any access. EACH is a pointer to the current structure being
94             # traversed on an "each" of the shadow hash, so that they can all be traversed
95             # in order. OVERRIDE is a hash containing values set directly by the user,
96             # which override anything in the shadow hash's underlying data structures.
97             # And finally, SOURCES is an array of the data structures (all Perl hashes,
98             # possibly tied).
99             sub TIEHASH {
100 2     2   1582 my $class = shift;
101 2   33     15 $class = ref $class || $class;
102 2         11 my $self = {
103             DELETED => {},
104             EACH => -1,
105             OVERRIDE => {},
106             SOURCES => []
107             };
108 2         5 bless ($self, $class);
109 2 50       11 $self->add (@_) if @_;
110 2         7 return $self;
111             }
112              
113             # Note that this doesn't work quite right in the case of keys with undefined
114             # values, but we can't make it work right since that would require using
115             # exists and a lot of common data sources (such as NDBM_File tied hashes)
116             # don't implement exists.
117             sub FETCH {
118 23     23   4378 my ($self, $key) = @_;
119 23 100       66 return if $self->{DELETED}{$key};
120 22 100       72 return $self->{OVERRIDE}{$key} if exists $self->{OVERRIDE}{$key};
121 16         18 for my $source (@{ $self->{SOURCES} }) {
  16         31  
122 18 100       143 return $source->{$key} if defined $source->{$key};
123             }
124 3         11 return;
125             }
126              
127             sub STORE {
128 8     8   1183 my ($self, $key, $value) = @_;
129 8         17 delete $self->{DELETED}{$key};
130 8         32 $self->{OVERRIDE}{$key} = $value;
131             }
132              
133             sub DELETE {
134 5     5   1681 my ($self, $key) = @_;
135 5         10 delete $self->{OVERRIDE}{$key};
136 5         20 $self->{DELETED}{$key} = 1;
137             }
138              
139             sub CLEAR {
140 5     5   1704 my ($self) = @_;
141 5         10 $self->{DELETED} = {};
142 5         12 $self->{OVERRIDE} = {};
143 5         11 $self->{SOURCES} = [];
144 5         23 $self->{EACH} = -1;
145             }
146              
147             # This could throw an exception if any underlying source doesn't support
148             # exists (like NDBM_File).
149             sub EXISTS {
150 2     2   387 my ($self, $key) = @_;
151 2 100       12 return if exists $self->{DELETED}{$key};
152 1         3 for my $source ($self->{OVERRIDE}, @{ $self->{SOURCES} }) {
  1         3  
153 2 50       8 return 1 if exists $source->{$key};
154             }
155 1         8 return;
156             }
157              
158             # We have to reset the each counter on all hashes. For tied hashes, we call
159             # FIRSTKEY directly because it's potentially more efficient than calling keys
160             # on the hash.
161             sub FIRSTKEY {
162 11     11   38 my ($self) = @_;
163 11         12 keys %{ $self->{OVERRIDE} };
  11         23  
164 11         14 for my $source (@{ $self->{SOURCES} }) {
  11         637  
165 20         22 my $tie = tied $source;
166 20 50       30 if ($tie) {
167 0         0 $tie->FIRSTKEY;
168             } else {
169 20         33 keys %$source;
170             }
171             }
172 11         22 $self->{EACH} = -1;
173 11         22 return $self->NEXTKEY;
174             }
175              
176             # Walk the sources by calling each on each one in turn, skipping deleted
177             # keys and keys shadowed by earlier hashes and using $self->{EACH} to
178             # store the number of source we're at.
179             sub NEXTKEY {
180 274     274   333 my ($self) = @_;
181 274         307 my @result = ();
182 317         882 SOURCE:
183 274   66     574 while (!@result && $self->{EACH} < @{ $self->{SOURCES} }) {
184 307 100       491 if ($self->{EACH} == -1) {
185 17         15 @result = each %{ $self->{OVERRIDE} };
  17         34  
186             } else {
187 290         276 @result = each %{ $self->{SOURCES}[$self->{EACH}] };
  290         1257  
188             }
189 307 100 100     1240 if (@result && $self->{DELETED}{$result[0]}) {
190 4         7 undef @result;
191 4         10 next;
192             }
193 303 100 100     1013 if (@result && $self->{EACH} > -1) {
194 267         279 my $key = $result[0];
195 267 100       480 if (exists $self->{OVERRIDE}{$key}) {
196 7         12 undef @result;
197 7         21 next;
198             }
199 260         537 for (my $index = $self->{EACH} - 1; $index >= 0; $index--) {
200 126 100       834 if (defined $self->{SOURCES}[$index]{$key}) {
201 2         3 undef @result;
202 2         6 next SOURCE;
203             }
204             }
205             }
206 294 50       1215 return (wantarray ? @result : $result[0]) if @result;
    100          
207 30         67 $self->{EACH}++;
208             }
209 10         81 return;
210             }
211              
212             ##############################################################################
213             # Module return value and documentation
214             ##############################################################################
215              
216             # Make sure the module returns true.
217             1;
218              
219             __DATA__