File Coverage

blib/lib/Sub/Trigger/Lock.pm
Criterion Covered Total %
statement 35 40 87.5
branch 5 8 62.5
condition n/a
subroutine 10 11 90.9
pod 4 4 100.0
total 54 63 85.7


line stmt bran cond sub pod time code
1 3     3   763874 use 5.008003;
  3         12  
  3         119  
2 3     3   18 use strict;
  3         5  
  3         203  
3 3     3   15 use warnings;
  3         11  
  3         190  
4              
5             package Sub::Trigger::Lock;
6              
7             our $AUTHORITY = 'cpan:TOBYINK';
8             our $VERSION = '0.001';
9              
10 3     3   2657 use Scope::Guard qw( guard );
  3         1433  
  3         206  
11 3     3   3305 use Exporter::Tiny qw( );
  3         7969  
  3         1416  
12              
13             our @ISA = qw(Exporter::Tiny);
14             our %EXPORT_TAGS = (
15             all => [qw( Lock RO lock unlock )],
16             default => [qw( Lock )],
17             );
18             our @EXPORT_OK = @{ $EXPORT_TAGS{all} };
19             our @EXPORT = @{ $EXPORT_TAGS{default} };
20              
21             sub _lock {
22            
23 6 100   6   5975 if ( ref($_[1]) eq 'ARRAY' ) {
24 5         24 &Internals::SvREADONLY( $_[1], 1 );
25 5         8 &Internals::SvREADONLY( \$_, 1 ) for @{$_[1]};
  5         42  
26 5         49 return;
27             }
28            
29 1 50       5 if ( ref($_[1]) eq 'HASH' ) {
30 1         6 &Internals::hv_clear_placeholders($_[1]);
31 1         4 &Internals::SvREADONLY( $_[1], 1 );
32 1         2 &Internals::SvREADONLY( \$_, 1 ) for values %{$_[1]};
  1         14  
33 1         2 return;
34             }
35            
36 0         0 return;
37             }
38              
39             sub Lock () {
40 4     4 1 4326 \&_lock;
41             }
42              
43             sub RO () {
44 2     2 1 31407 'ro', 'trigger', Lock;
45             }
46              
47             sub lock ($) {
48 0     0 1 0 _lock(undef, @_);
49             }
50              
51             sub unlock ($) {
52 2     2 1 4854 my $ref = shift;
53            
54 2 50       12 if ( ref($ref) eq 'ARRAY' ) {
55 2         7 &Internals::SvREADONLY( $ref, 0 );
56 2         13 &Internals::SvREADONLY( \$_, 0 ) for @$ref;
57             }
58            
59 2 50       19 if ( ref($_[1]) eq 'HASH' ) {
60 0         0 &Internals::hv_clear_placeholders($ref);
61 0         0 &Internals::SvREADONLY( $ref, 0 );
62 0         0 &Internals::SvREADONLY( \$_, 0 ) for values %$ref;
63             }
64            
65 2     2   20 return guard { _lock(undef, $ref) };
  2         111  
66             }
67              
68             1;
69              
70             __END__
71              
72             =pod
73              
74             =encoding utf-8
75              
76             =head1 NAME
77              
78             Sub::Trigger::Lock - a coderef for use in Moose triggers that will lock hashrefs and arrayrefs
79              
80             =head1 SYNOPSIS
81              
82             This module provides the antidote for:
83              
84             package Foo {
85             use Moose;
86            
87             has bar => (is => 'ro', isa => 'ArrayRef');
88             }
89            
90             my $foo = Foo->new( bar => [1,2,3] );
91             push @{ $foo->bar }, 4; # does not die!
92              
93             All you need to do is:
94              
95             package Foo {
96             use Moose;
97             use Sub::Trigger::Lock;
98            
99             has bar => (is => 'ro', isa => 'ArrayRef', trigger => Lock);
100             }
101              
102             Or, a shortcut:
103              
104             package Foo {
105             use Moose;
106             use Sub::Trigger::Lock qw(RO);
107            
108             has bar => (is => RO, isa => 'ArrayRef');
109             }
110              
111             =head1 TL;DR
112              
113             Force modifications of your arrayref/hashref attributes to be made via
114             your documented API.
115              
116             =head1 DESCRIPTION
117              
118             This module provides two constants, C<Lock> and C<RO>. The first of
119             these is the only one exported by default, and the key to understanding
120             this module. This module also provides the utility functions C<lock>
121             and C<unlock>, which are not exported by default.
122              
123             =over
124              
125             =item C<< Lock >>
126              
127             C<Lock> is a constant which evaluates to a coderef. The coderef takes
128             two or more arguments. That is, C<Lock> itself takes no arguments; it
129             returns a coderef that takes arguments!
130              
131             The first argument is supposed to be a blessed object, but it is
132             actually completely ignored.
133              
134             If the second argument is not a hashref or arrayref, it is also
135             ignored. Everything is ignored! But if the second argument I<is>
136             a hashref or arrayref, it will be flagged as read-only.
137              
138             This is a fairly shallow read-only flag. Attempts to add or remove
139             keys from the hash, or change the value for any key will throw an
140             exception. But if the value is reference to some other structure, that
141             structure will be unaffected.
142              
143             Similarly, attempts to push, pop, shift, or unshift a read-only array,
144             or to change the value for any index will throw an exception. Buf if
145             the values are references to other structures, these will also be
146             unaffected.
147              
148             Overall, the effect of C<Lock> is that you can do something like this:
149              
150             package Person {
151             use Moose;
152            
153             has name => (is => 'ro', writer => 'set_name');
154             }
155            
156             package Band {
157             use Moose;
158             use Sub::Trigger::Lock;
159            
160             has members => (is => 'ro', trigger => Lock);
161             }
162            
163             my $spice_girls = Band->new(
164             members => [
165             Person->new(name => 'Victoria Adams'),
166             Person->new(name => 'Melanie Brown'),
167             Person->new(name => 'Emma Bunton'),
168             Person->new(name => 'Melanie Chisholm'),
169             Person->new(name => 'Geri Halliwell'),
170             ],
171             );
172            
173             # This is OK, because deep changes work
174             $spice_girls->members->[0]->set_name('Victoria Beckham');
175            
176             # This is not OK, because shallow changes throw!
177             $spice_girls->members->[0] = Person->new(name => 'Johnny Cash');
178              
179             =item C<< RO >>
180              
181             C<RO> is a constant that evaluates to the list:
182              
183             'ro', 'trigger', Lock,
184              
185             =item C<< lock($ref) >>
186              
187             A utility function for locking an arrayref or hashref in the same way
188             that the C<Lock> coderef would.
189              
190             =item C<< unlock($ref) >>
191              
192             A utility function for unlocking an arrayref or hashref.
193              
194             Note that this returns a I<< guard object >>. You should store this
195             object in a variable. Once the object is destroyed (e.g. because the
196             variable has gone out of scope), C<< $ref >> will be automatically
197             locked again!
198              
199             This allows you to temporarily unlock a hashref or arrayref in order
200             to privately manipulate it:
201              
202             package Band {
203             use Moose;
204             use Sub::Trigger::Lock qw( Lock unlock );
205            
206             has members => (is => 'ro', trigger => Lock);
207            
208             sub add_members {
209             my ($self, @members) = @_;
210             my $guard = unlock( $self->members );
211             push @{$self->members}, @members;
212             }
213             }
214              
215             =back
216              
217             =head1 IMPLEMENTATION NOTES
218              
219             This module uses the Perl internal C<< Internals::SvREADONLY >>
220             function for most of the heavy lifting. This is much, much faster
221             than ties.
222              
223             =head1 BUGS
224              
225             Please report any bugs to
226             L<http://rt.cpan.org/Dist/Display.html?Queue=Sub-Trigger-Lock>.
227              
228             =head1 SEE ALSO
229              
230             L<Exporter::Tiny>, L<Scope::Guard>.
231              
232             =head1 AUTHOR
233              
234             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
235              
236             =head1 COPYRIGHT AND LICENCE
237              
238             This software is copyright (c) 2014 by Toby Inkster.
239              
240             This is free software; you can redistribute it and/or modify it under
241             the same terms as the Perl 5 programming language system itself.
242              
243             =head1 DISCLAIMER OF WARRANTIES
244              
245             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
246             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
247             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
248