File Coverage

blib/lib/Tie/Locked.pm
Criterion Covered Total %
statement 117 133 87.9
branch 15 20 75.0
condition 8 13 61.5
subroutine 33 37 89.1
pod 11 12 91.6
total 184 215 85.5


line stmt bran cond sub pod time code
1             package Tie::Locked;
2 1     1   661 use strict;
  1         1  
  1         37  
3 1     1   5 use Carp 'croak';
  1         2  
  1         97  
4            
5            
6             # version
7             our $VERSION = '1.1';
8            
9            
10             # debugging tools
11             # use Debug::ShowStuff ':all';
12             # use Debug::ShowStuff::ShowVar;
13            
14             =head1 NAME
15            
16             Tie::Locked -- lock hashes so that they cannot be easily changed
17            
18             =head1 SYNOPSIS
19            
20             use Tie::Locked ':all';
21            
22             # creates locked hash with initial value x=>1
23             tie %hash, 'Tie::Locked::Tied', x=>1;
24            
25             # get tied hashref with initial value x=>1
26             my $ref = locked_hashref('x'=>1);
27            
28             # the following commands cause fatal errors
29             print $ref->{'y'}; # references non-existent key
30             $ref->{'y'} = 'yyyyy'; # assigns to non-existent key
31             $ref->{'x'} = 'yyyyy'; # assigns to existent key
32            
33             # but this command is ok
34             $dummy = $ref->{'x'}; # references existent key
35            
36             # get unlocked hashref
37             my $ref = locked_hashref('x'=>1);
38            
39             # the following commands do NOT cause errors because the hash isn't locked
40             print $ref->{'y'};
41             $ref->{'y'} = 'yyyyy';
42            
43             # now lock the hashref
44             $ref->lock;
45            
46             # many other features...
47            
48             =head1 DESCRIPTION
49            
50             Tie::Locked allows you to create hashes in which the values of the hash cannot
51             be easily changed. If an element that does not exist is referenced then the
52             code croaks. Tie::Locked is useful for situations where you want to make sure
53             your code doesn't accidentally change values. If code attempts to change or
54             delete an existing element, then the code dies.
55            
56             I created Tier::Locked when I wrote buggy code something like this:
57            
58             my $whatever = {};
59            
60             # a bunch of code that, under some conditions, never creates or sets
61             # the value $whatever->{'done'}
62            
63             if (! $whatever->{'done'}) {
64             ...
65             }
66            
67             It took an hour of debugging to figure out, so I created this module to avoid
68             losing more time from things I'd rather do, like write non-buggy code.
69            
70             Please note: I never actually use Tie::Locked to tie hashes directly. I use
71             locked_hashref() and unlocked_hashref() to get hash references. This
72             documentation is going to focus on that usage.
73            
74             =head1 INSTALLATION
75            
76             Tie::Locked can be installed with the usual routine:
77            
78             perl Makefile.PL
79             make
80             make test
81             make install
82            
83             =head1 FUNCTIONS
84            
85             =cut
86            
87            
88             # Works like a regular hash, except that no changes are allowed to the keys
89             # or values once the hash has been locked. Also, croaks when an attempt is made
90             # to retrieve a nonexistent key.
91            
92             # export
93 1     1   5 use vars qw[@EXPORT_OK %EXPORT_TAGS];
  1         5  
  1         73  
94 1     1   6 use base 'Exporter';
  1         1  
  1         846  
95             @EXPORT_OK = qw[ locked_hashref unlocked_hashref ];
96             %EXPORT_TAGS = ('all' => [@EXPORT_OK]);
97            
98            
99             #------------------------------------------------------------------------------
100             # locked_hashref
101             #
102            
103             =head2 locked_hashref
104            
105             locked_hashref() returns a reference to a locked hash. All options sent to
106             locked_hashref() are set as the locked values of the hash. So, for example,
107             the following code creates a hashref with one key 'x' with a value of 1:
108            
109             my $ref = locked_hashref('x'=>1);
110            
111             =cut
112            
113             sub locked_hashref {
114 5     5 1 188 my ($self, %hash);
115            
116 5         15 tie %hash, 'Tie::Locked::Tied', @_;
117 5         9 $self = \%hash;
118 5         8 bless $self, 'Tie::Locked';
119 5         8 return $self;
120             }
121             #
122             # locked_hashref
123             #------------------------------------------------------------------------------
124            
125            
126             #------------------------------------------------------------------------------
127             # unlocked_hashref
128             #
129            
130             =head2 unlocked_hashref
131            
132             unlocked_hashref() returns a reference to an unlocked hash. This is useful for
133             the situation where you want to initialize the values in the hash before
134             locking it. For example, the following code creates a Tie::Locked object, sets
135             some values in it, then locks the hash.
136            
137             my $ref = unlocked_hashref();
138            
139             $ref->{'Mbala'} = 1;
140             $ref->{'Josh'} = 2;
141             $ref->{'Starflower'} = 3;
142            
143             $ref->lock();
144            
145             =cut
146            
147             sub unlocked_hashref {
148 1     1 1 74 my $self = locked_hashref(@_);
149 1         4 $self->unlock();
150 1         3 return $self;
151             }
152             #
153             # unlocked_hashref
154             #------------------------------------------------------------------------------
155            
156            
157             #------------------------------------------------------------------------------
158             # $ref->lock()
159             #
160            
161             =head2 $ref->lock()
162            
163             The lock() method (not to be confused with Perl's native lock function) locks
164             the Tie::Locked object. For example, the following code creates an unlocked
165             Tie::Locked object, sets some values, then locks the hash.
166            
167             my $ref = unlocked_hashref();
168            
169             $ref->{'Mbala'} = 1;
170             $ref->{'Josh'} = 2;
171             $ref->{'Starflower'} = 3;
172            
173             $ref->lock();
174            
175             =cut
176            
177             sub lock {
178 3     3 1 37 my ($self) = @_;
179 3         4 return tied(%{$self})->lock;
  3         9  
180             }
181             #
182             # $ref->lock()
183             #------------------------------------------------------------------------------
184            
185            
186             #------------------------------------------------------------------------------
187             # $ref->unlock()
188             #
189            
190             =head2 $ref->unlock()
191            
192             Unlocks the hash. For example, the following code unlocks the hash, sets some
193             values, then relocks it.
194            
195             $ref->unlock();
196             $ref->{'x'} = 'yyyyy';
197             $ref->{'z'} = 'yyyyy';
198            
199             # relock
200             $ref->lock;
201            
202             =cut
203            
204             sub unlock {
205 3     3 1 8 my ($self) = @_;
206 3         3 my $locked = tied(%{$self});
  3         4  
207            
208 3 50       9 if (! $locked) {
209             # dietrace title=>'no tied reference';
210 0         0 croak 'no tied reference to Tie::Locked::Tied';
211             }
212            
213 3         4 return tied(%{$self})->unlock;
  3         7  
214             }
215             #
216             # $ref->unlock()
217             #------------------------------------------------------------------------------
218            
219            
220            
221             #------------------------------------------------------------------------------
222             # $ref->autolocker()
223             #
224            
225             =head2 $ref->autolocker()
226            
227             autolocker() unlocks the hash and returns an object that, when it goes out of
228             scope, relocks the hash. This is useful for situations where you want to
229             unlock the hash and be sure it gets relocked even if the routine exits midway.
230            
231             For example, the following code creates an autolocker object in the do{} block,
232             so setting the hash does not cause an error in that block. However, after the
233             locker has gone out of scope, the hash is locked again.
234            
235             my $ref = locked_hashref('x'=>1);
236            
237             do {
238             my $locker = $ref->autolocker();
239             $ref->{'steve'} = 1; # does not cause an error
240             };
241            
242             $ref->{'fred'} = 2; # causes an error
243            
244             =cut
245            
246             sub autolocker {
247 1     1 1 2 my ($self) = @_;
248 1         2 $self->unlock();
249 1         7 return Tie::Locked::AutoLocker->new($self);
250             }
251            
252             # alias auto_locker to autolocker
253             sub auto_locker {
254 0     0 0 0 my $self = shift;
255 0         0 return $self->autolocker(@_);
256             }
257            
258             #
259             # $ref->autolocker()
260             #------------------------------------------------------------------------------
261            
262            
263             #------------------------------------------------------------------------------
264             # locked
265             #
266            
267             =head2 $ref->locked()
268            
269             Returns true if the hash is locked.
270            
271             =cut
272            
273             sub locked {
274 4     4 1 13 my ($self) = @_;
275 4         4 return tied(%{$self})->locked;
  4         10  
276             }
277             #
278             # locked
279             #------------------------------------------------------------------------------
280            
281            
282            
283             #------------------------------------------------------------------------------
284             # unlocked
285             #
286            
287             =head2 $ref->unlocked()
288            
289             Returns true if the hash is not locked.
290            
291             =cut
292            
293             sub unlocked {
294 4     4 1 6 my ($self) = @_;
295 4         5 return tied(%{$self})->unlocked;
  4         26  
296             }
297             #
298             # unlocked
299             #------------------------------------------------------------------------------
300            
301            
302             #------------------------------------------------------------------------------
303             # unlock_fields
304             #
305            
306             =head2 $ref->unlock_fields(I, I, ...)
307            
308             This method allows you to unlock just specific fields in the hash. For
309             example, in the following code, the fields 'first', 'middle', and 'last' are
310             unlocked, but the id field is not. Notice that the fields do not need to
311             actually exist in order to be unlocked.
312            
313             # create customer hash
314             my $customer = locked_hashref(id=>'3245');
315            
316             # unlock name fields
317             $customer->unlock_fields('first', 'middle', 'last');
318            
319             # set name fields - does not cause any errors
320             $customer->{'first'} = 'Michael';
321             $customer->{'middle'} = 'Jadin';
322             $customer->{'last'} = 'Forsyth';
323            
324             # but this line causes an error:
325             $customer->{'id'} = 2087;
326            
327             Each call to unlock_fields() adds to the list of unlocked fields, so the
328             following code accomplishes the same thing as above.
329            
330             $customer->unlock_fields('first');
331             $customer->unlock_fields('middle');
332             $customer->unlock_fields('last');
333            
334             =cut
335            
336             sub unlock_fields {
337 1     1 1 4 my $self = shift;
338 1         1 my $tied = tied(%{$self});
  1         2  
339            
340 1         2 return tied(%{$self})->unlock_fields(@_);
  1         3  
341             }
342             #
343             # unlock_fields
344             #------------------------------------------------------------------------------
345            
346            
347             #------------------------------------------------------------------------------
348             # lock_fields
349             #
350            
351             =head2 $ref->lock_fields(I, I, ...)
352            
353             The opposite of unlock_fields(), this method locks the given fields.
354            
355             =cut
356            
357             sub lock_fields {
358 1     1 1 77 my $self = shift;
359 1         1 my $tied = tied(%{$self});
  1         2  
360            
361 1         1 return tied(%{$self})->lock_fields(@_);
  1         11  
362             }
363             #
364             # lock_fields
365             #------------------------------------------------------------------------------
366            
367            
368             #------------------------------------------------------------------------------
369             # lock_all_fields
370             #
371            
372             =head2 $ref->lock_all_fields()
373            
374             Locks all fields.
375            
376             =cut
377            
378             sub lock_all_fields {
379 1     1 1 52 my $self = shift;
380 1         1 my $tied = tied(%{$self});
  1         2  
381            
382 1         2 return tied(%{$self})->lock_all_fields(@_);
  1         3  
383             }
384             #
385             # lock_all_fields
386             #------------------------------------------------------------------------------
387            
388            
389             #------------------------------------------------------------------------------
390             # unlocked_fields
391             #
392            
393             =head2 $ref->unlocked_fields()
394            
395             Returns an array of fields that are not locked.
396            
397             =cut
398            
399             sub unlocked_fields {
400 3     3 1 41 my $self = shift;
401 3         4 my $tied = tied(%{$self});
  3         4  
402            
403 3         4 return tied(%{$self})->unlocked_fields(@_);
  3         8  
404             }
405             #
406             # unlocked_fields
407             #------------------------------------------------------------------------------
408            
409            
410            
411             =head1 Known bugs
412            
413             The autolocker object does not go out of scope if it is created in a
414             one-command block. For example, the following code does NOT cause an error
415             even though it should.
416            
417             my $ref = locked_hashref();
418            
419             do {
420             my $locker = $ref->autolocker();
421             };
422            
423             # does not cause an error
424             $ref->{'Steve'} = 4;
425            
426            
427             =cut
428            
429            
430             ###############################################################################
431             # Tie::Locked::Tied
432             #
433             package Tie::Locked::Tied;
434 1     1   6 use strict;
  1         2  
  1         28  
435 1     1   5 use Carp 'croak';
  1         1  
  1         1022  
436            
437             # debugging tools
438             # use Debug::ShowStuff ':all';
439            
440             sub TIEHASH {
441 6     6   21 my $class = shift;
442 6         25 my $self = bless {data=>{@_}}, $class;
443            
444 6         16 $self->{'locked'} = 1;
445            
446 6         13 return $self;
447             }
448            
449             sub EXISTS {
450 2     2   90 my ($self, $key) = @_;
451 2         7 return exists $self->{'data'}->{$key};
452             }
453            
454             sub FETCH {
455 11     11   454 my ($self, $key) = @_;
456            
457 11 100       27 if ($self->{'locked'}) {
458 9 100       21 if (! exists $self->{'data'}->{$key}) {
459 5         8 my $ul = $self->{'unlocked_fields'};
460            
461 5 100 66     16 unless ($ul && exists($ul->{$key}) ) {
462             # dietrace title=>"no key named '$key'";
463 4         435 croak "no key named '$key'";
464             }
465             }
466             }
467            
468 7         26 return $self->{'data'}->{$key};
469             }
470            
471             sub FIRSTKEY {
472 0     0   0 my $self = shift;
473 0         0 my $a = keys %{$self->{'data'}};
  0         0  
474 0         0 return scalar each %{$self->{'data'}};
  0         0  
475             }
476            
477             sub NEXTKEY {
478 0     0   0 my $self = shift;
479 0         0 return scalar each %{$self->{'data'}};
  0         0  
480             }
481            
482             sub CLEAR {
483 0     0   0 my ($self) = @_;
484            
485 0 0       0 if ($self->{'locked'}) {
486             # dietrace title=>'cannot clear locked ' . ref($self) . ' hash';
487 0         0 croak 'cannot clear locked ' . ref($self) . ' hash';
488             }
489            
490             else {
491 0         0 $self->{'data'} = {};
492             }
493             }
494            
495             sub STORE {
496 15     15   657 my ($self, $key, $datum) = @_;
497            
498 15 100       36 if ($self->{'locked'}) {
499 11         12 my $ul = $self->{'unlocked_fields'};
500            
501 11 100 100     32 unless ($ul && exists($ul->{$key}) ) {
502             # dietrace title=>'cannot store "' . $key . '" into locked ' . ref($self) . ' hash';
503 10         849 croak 'cannot store "' . $key . '" into locked ' . ref($self) . ' hash';
504             }
505             }
506            
507 5         17 $self->{'data'}->{$key} = $datum;
508             }
509            
510             sub DELETE {
511 1     1   38 my ($self, $key) = @_;
512            
513 1 50       4 if ($self->{'locked'}) {
514 1         1 my $ul = $self->{'unlocked_fields'};
515            
516 1 50 33     16 unless ($ul && exists($ul->{$key}) )
517 0         0 { 'cannot delete from locked ' . ref($self) . ' hash' }
518             }
519            
520 1         5 delete $self->{'data'}->{$key};
521             }
522            
523            
524 3     3   8 sub lock {$_[0]->{'locked'} = 1}
525 3     3   8 sub unlock {$_[0]->{'locked'} = 0}
526            
527 8     8   30 sub locked {return $_[0]->{'locked'}}
528 4     4   9 sub unlocked {return ! $_[0]->locked}
529            
530             sub unlock_fields {
531 1     1   2 my ($self, @fields) = @_;
532 1   50     14 my $uls = $self->{'unlocked_fields'} ||= {};
533            
534 1         2 @{$uls}{@fields} = ();
  1         4  
535             }
536            
537             sub unlocked_fields {
538 3     3   4 my ($self) = @_;
539 3         4 my (@rv);
540            
541 3 100       7 if ($self->{'unlocked_fields'})
542 2         3 { @rv = keys(%{$self->{'unlocked_fields'}}) }
  2         5  
543            
544             # return
545 3         10 return @rv;
546             }
547            
548             sub lock_fields {
549 1     1   2 my ($self, @fields) = @_;
550 1   50     4 my $uls = $self->{'unlocked_fields'} ||= {};
551            
552 1         2 foreach my $field (@fields)
553 1         5 { delete $uls->{$field} }
554             }
555            
556             sub lock_all_fields {
557 1     1   2 my ($self, @fields) = @_;
558 1         5 delete $self->{'unlocked_fields'};
559             }
560            
561             #
562             # Tie::Locked::Tied
563             ###############################################################################
564            
565            
566            
567             ###############################################################################
568             # Tie::Locked::AutoLocker
569             #
570             package Tie::Locked::AutoLocker;
571 1     1   5 use strict;
  1         2  
  1         138  
572            
573             # debugging tools
574             # use Debug::ShowStuff ':all';
575            
576             sub new {
577 1     1   2 my ($class, $locked) = @_;
578 1         2 my $self = bless {}, $class;
579 1         5 $self->{'locked'} = $locked;
580 1         3 return $self;
581             }
582            
583             DESTROY {
584 1     1   26 my ($self) = @_;
585            
586 1         3 $self->{'locked'}->lock();
587             }
588            
589             #
590             # Tie::Locked::AutoLocker
591             ###############################################################################
592            
593            
594             # return true
595             1;
596            
597             __END__