File Coverage

blib/lib/Types/ReadOnly.pm
Criterion Covered Total %
statement 42 44 95.4
branch 8 10 80.0
condition 10 15 66.6
subroutine 12 12 100.0
pod n/a
total 72 81 88.8


line stmt bran cond sub pod time code
1 6     6   668596 use 5.008;
  6         55  
2 6     6   29 use strict;
  6         10  
  6         113  
3 6     6   38 use warnings;
  6         7  
  6         330  
4              
5             package Types::ReadOnly;
6              
7             our $AUTHORITY = 'cpan:TOBYINK';
8             our $VERSION = '0.003';
9              
10 6     6   551 use Type::Tiny 1.006000 ();
  6         14680  
  6         98  
11 6     6   459 use Type::Coercion ();
  6         5224  
  6         98  
12 6     6   506 use Types::Standard qw( Any Dict HashRef Ref );
  6         41240  
  6         36  
13 6     6   4553 use Type::Library -base, -declare => qw( ReadOnly Locked );
  6         10  
  6         54  
14              
15 6     6   3070 use Scalar::Util qw( reftype blessed refaddr );
  6         12  
  6         457  
16              
17             sub _dclone($) {
18 1     1   564 require Storable;
19 6     6   31 no warnings 'redefine';
  6         10  
  6         8746  
20 1         2768 *_dclone = \&Storable::dclone;
21 1         60 goto &Storable::dclone;
22             }
23              
24             my %skip = map { $_ => 1 } qw/CODE GLOB/;
25             sub _make_readonly {
26 16     16   3056 my (undef, $dont_clone) = @_;
27 16 100 66     91 if (my $reftype = reftype $_[0] and not blessed($_[0]) and not &Internals::SvREADONLY($_[0])) {
      100        
28 6 100 66     137 $_[0] = _dclone($_[0]) if !$dont_clone && &Internals::SvREFCNT($_[0]) > 1 && !$skip{$reftype};
      66        
29 6         23 &Internals::SvREADONLY($_[0], 1);
30 6 50 33     42 if ($reftype eq 'SCALAR' or $reftype eq 'REF') {
    100          
    50          
31 0         0 _make_readonly(${ $_[0] }, 1);
  0         0  
32             }
33             elsif ($reftype eq 'ARRAY') {
34 3         6 _make_readonly($_) for @{ $_[0] };
  3         14  
35             }
36             elsif ($reftype eq 'HASH') {
37 3         9 &Internals::hv_clear_placeholders($_[0]);
38 3         4 _make_readonly($_) for values %{ $_[0] };
  3         16  
39             }
40             }
41 16         31 Internals::SvREADONLY($_[0], 1);
42 16         36 return;
43             }
44              
45             __PACKAGE__->meta->add_type({
46             name => 'ReadOnly',
47             parent => Ref,
48             constraint => sub {
49             my $r = reftype($_);
50             ($r eq 'HASH' or $r eq 'ARRAY' or $r eq 'SCALAR' or $r eq 'REF') and &Internals::SvREADONLY($_);
51             },
52             constraint_generator => sub {
53             my ($parameter) = @_ or return $Type::Tiny::parameterize_type;
54             $parameter->compiled_check; # only need this because parent constraint (i.e. ReadOnly) is automatically checked
55             },
56             inlined => sub {
57             my ($self, $varname) = @_;
58             return (
59             sprintf('do { my $r = Scalar::Util::reftype(%s); $r eq "HASH" or $r eq "ARRAY" or $r eq "SCALAR" or $r eq "REF" }', $varname),
60             sprintf('&Internals::SvREADONLY(%s)', $varname),
61             );
62             },
63             inline_generator => sub {
64             my ($parameter) = @_ or return $Type::Tiny::parameterize_type;
65             return unless $parameter->can_be_inlined;
66             sub {
67             my ($child, $varname) = @_;
68             my $me = $child->parent;
69             return ($me->inline_check($varname), $parameter->inline_check($varname));
70             };
71             },
72             coercion => [
73             Ref ,=> 'do { Types::ReadOnly::_make_readonly(my $ro = $_); $ro }',
74             ],
75             coercion_generator => sub {
76             my ($me, $child) = @_;
77             my $parameter = $child->type_parameter;
78             my @extra;
79             if ($parameter->has_coercion) {
80             my @map = @{ $parameter->coercion->type_coercion_map };
81             while (@map) {
82             my ($t, $code) = splice @map, 0, 2;
83             if (Types::TypeTiny::CodeLike->check($code)) {
84             push @extra, $t, sub {
85             my $coerced = $code->(@_);
86             Types::ReadOnly::_make_readonly($coerced);
87             $coerced;
88             };
89             }
90             else {
91             push @extra, $t, sprintf('do { my $coerced = %s; Types::ReadOnly::_make_readonly($coerced); $coerced }', $code);
92             }
93             }
94             }
95             bless(
96             { type_coercion_map => [
97             $parameter => 'do { Types::ReadOnly::_make_readonly(my $ro = $_); $ro }',
98             @extra,
99             ] },
100             'Type::Coercion'
101             );
102             },
103             });
104              
105             my $_FIND_KEYS = sub {
106             my ($dict) = grep {
107             $_->is_parameterized
108             and $_->has_parent
109             and $_->parent->strictly_equals(Dict)
110             } $_[0], $_[0]->parents;
111             return unless $dict;
112             return if ref($dict->parameters->[-1]) eq q(HASH);
113             my @keys = sort keys %{ +{ @{ $dict->parameters } } };
114             return unless @keys;
115             \@keys;
116             };
117              
118             # Stolen from Hash::Util 0.15.
119             # In earlier versions, of Hash::Util, there is only a hashref_unlocked
120             # function, which happens to be very broken. :-/
121 3     3   4146 sub _hashref_locked { &Internals::SvREADONLY($_[0]) }
122              
123             __PACKAGE__->meta->add_type({
124             name => 'Locked',
125             parent => Ref['HASH'],
126             constraint => sub {
127             my $r = reftype($_);
128             &Internals::SvREADONLY($_);
129             },
130             constraint_generator => sub {
131             my ($parameter) = @_ or return $Type::Tiny::parameterize_type;
132             my $pchk = $parameter->compiled_check;
133             my $KEYS = $parameter->$_FIND_KEYS or return $pchk;
134             my $keys = join "*#*", @$KEYS;
135             sub {
136             my $legal = join "*#*", sort(&Hash::Util::legal_keys($_));
137             return if $keys ne $legal;
138             goto $pchk;
139             };
140             },
141             inlined => sub {
142             my ($self, $varname) = @_;
143             my $r = Ref['HASH'];
144             return (
145             $r->inline_check($varname),
146             sprintf('&Internals::SvREADONLY(%s)', $varname),
147             );
148             },
149             inline_generator => sub {
150             require Hash::Util;
151             my ($parameter) = @_ or return $Type::Tiny::parameterize_type;
152             return unless $parameter->can_be_inlined;
153             my $KEYS = $parameter->$_FIND_KEYS;
154             my $keys = join "*#*", @{ $KEYS || [] };
155             sub {
156             my ($child, $varname) = @_;
157             my @extras;
158             if ($keys) {
159             require Hash::Util;
160             push @extras, sprintf('%s eq join("*#*", sort(&Hash::Util::legal_keys(%s)))', B::perlstring($keys), $varname);
161             }
162             (undef, @extras, $parameter->inline_check($varname));
163             };
164             },
165             coercion => [
166             Ref['HASH'] , => 'do { Types::ReadOnly::_make_readonly(my $ro = $_); $ro }',
167             ],
168             coercion_generator => sub {
169             require Hash::Util;
170             my ($me, $child) = @_;
171             my $parameter = $child->type_parameter;
172             my $KEYS = $parameter->$_FIND_KEYS;
173             my $qkeys = $KEYS ? join(q[,], '', map B::perlstring($_), @$KEYS) : '';
174             my @extra;
175             if ($parameter->has_coercion) {
176             my @map = @{ $parameter->coercion->type_coercion_map };
177             while (@map) {
178             my ($t, $code) = splice @map, 0, 2;
179             if (Types::TypeTiny::CodeLike->check($code)) {
180             push @extra, $t, sub {
181             my $coerced = $code->(@_);
182             &Hash::Util::unlock_hash($coerced);
183             &Hash::Util::lock_hash($coerced, @{$KEYS||[]});
184             $coerced;
185             };
186             }
187             else {
188             push @extra, $t, sprintf('do { my $coerced = %s; &Hash::Util::unlock_hash($coerced); &Hash::Util::lock_keys($coerced %s); $coerced }', $code, $qkeys);
189             }
190             }
191             }
192             bless(
193             { type_coercion_map => [
194             $parameter => sprintf('do { my $coerced = $_; &Hash::Util::unlock_hash($coerced); &Hash::Util::lock_keys($coerced %s); $coerced }', $qkeys),
195             @extra,
196             ] },
197             'Type::Coercion'
198             );
199             },
200             });
201              
202              
203             # This comparator allows Locked[Foo] to be seen as a child of Foo, and not
204             # just a child of Locked. It's probably not foolproof.
205             #
206             my $comparator;
207             $comparator = sub {
208             my $A = shift->find_constraining_type;
209             my $B = shift->find_constraining_type;
210             my $RO = __PACKAGE__->get_type('ReadOnly');
211             my $L = __PACKAGE__->get_type('Locked');
212            
213             my $Aprime = $A->find_parent(sub {
214             $_->is_parameterized and
215             $_->has_parent and
216             $_->parent->strictly_equals($L) || $_->parent->strictly_equals($RO)
217             });
218            
219             if ($Aprime) {
220             my $param = $Aprime->type_parameter->find_constraining_type;
221             if ($param->is_a_type_of($B)) {
222             return Type::Tiny::CMP_SUBTYPE();
223             }
224             }
225            
226             return Type::Tiny::CMP_UNKNOWN() if @_;
227            
228             my $r = $comparator->($B, $A, 1);
229             return $r if ($r eq Type::Tiny::CMP_EQUIVALENT());
230             return -$r if ($r eq Type::Tiny::CMP_SUPERTYPE() || $r eq Type::Tiny::CMP_SUBTYPE());
231            
232             Type::Tiny::CMP_UNKNOWN();
233             };
234              
235             push @Type::Tiny::CMP, $comparator;
236              
237             __PACKAGE__->meta->make_immutable;
238              
239              
240             __END__