add a description to a test which did not have one
[gitmo/Class-MOP.git] / lib / Class / MOP / Immutable.pm
CommitLineData
c23184fc 1
2package Class::MOP::Immutable;
3
4use strict;
5use warnings;
6
7use Class::MOP::Method::Constructor;
8
9use Carp 'confess';
10use Scalar::Util 'blessed';
11
34147f49 12our $VERSION = '0.71_01';
d519662a 13$VERSION = eval $VERSION;
c23184fc 14our $AUTHORITY = 'cpan:STEVAN';
15
d7b2249e 16use base 'Class::MOP::Object';
17
0ac992ee 18sub new {
1ae8e211 19 my ($class, @args) = @_;
0ac992ee 20
1ae8e211 21 my ( $metaclass, $options );
22
23 if ( @args == 2 ) {
24 # compatibility args
25 ( $metaclass, $options ) = @args;
26 } else {
27 unshift @args, "metaclass" if @args % 2 == 1;
28
29 # default named args
30 my %options = @args;
31 $options = \%options;
32 $metaclass = $options{metaclass};
33 }
34
0bfc85b8 35 my $self = $class->_new(
8683db0e 36 'metaclass' => $metaclass,
37 'options' => $options,
38 'immutable_metaclass' => undef,
0bfc85b8 39 );
0ac992ee 40
c23184fc 41 return $self;
42}
43
0bfc85b8 44sub _new {
45 my $class = shift;
46 my $options = @_ == 1 ? $_[0] : {@_};
47
48 bless $options, $class;
49}
50
76c20e30 51sub immutable_metaclass {
52 my $self = shift;
53
54 $self->create_immutable_metaclass unless $self->{'immutable_metaclass'};
55
56 return $self->{'immutable_metaclass'};
57}
58
8683db0e 59sub metaclass { (shift)->{'metaclass'} }
60sub options { (shift)->{'options'} }
c23184fc 61
62sub create_immutable_metaclass {
63 my $self = shift;
64
65 # NOTE:
0ac992ee 66 # The immutable version of the
c23184fc 67 # metaclass is just a anon-class
0ac992ee 68 # which shadows the methods
c23184fc 69 # appropriately
8683db0e 70 $self->{'immutable_metaclass'} = Class::MOP::Class->create_anon_class(
c23184fc 71 superclasses => [ blessed($self->metaclass) ],
72 methods => $self->create_methods_for_immutable_metaclass,
0ac992ee 73 );
c23184fc 74}
75
d9586da2 76
c23184fc 77my %DEFAULT_METHODS = (
d9586da2 78 # I don't really understand this, but removing it breaks tests (groditi)
0ac992ee 79 meta => sub {
c23184fc 80 my $self = shift;
0ac992ee 81 # if it is not blessed, then someone is asking
127d39a7 82 # for the meta of Class::MOP::Immutable
c23184fc 83 return Class::MOP::Class->initialize($self) unless blessed($self);
0ac992ee 84 # otherwise, they are asking for the metaclass
c23184fc 85 # which has been made immutable, which is itself
84bc89b3 86 # except in the cases where it is a metaclass itself
87 # that has been made immutable and for that we need
88 # to dig a bit ...
89 if ($self->isa('Class::MOP::Class')) {
90 return $self->{'___original_class'}->meta;
91 }
92 else {
93 return $self;
94 }
c23184fc 95 },
d9586da2 96 is_mutable => sub { 0 },
97 is_immutable => sub { 1 },
98 make_immutable => sub { () },
c23184fc 99);
100
101# NOTE:
0ac992ee 102# this will actually convert the
103# existing metaclass to an immutable
c23184fc 104# version of itself
105sub make_metaclass_immutable {
229910b5 106 my ($self, $metaclass, $options) = @_;
107
1a84e3f3 108 my %options = (
109 inline_accessors => 1,
110 inline_constructor => 1,
111 inline_destructor => 0,
112 constructor_name => 'new',
113 debug => 0,
114 %$options,
115 );
116
117 %$options = %options; # FIXME who the hell is relying on this?!? tests fail =(
0ac992ee 118
75f173e5 119 $self->_inline_accessors( $metaclass, \%options );
120 $self->_inline_constructor( $metaclass, \%options );
121 $self->_inline_destructor( $metaclass, \%options );
bc79f8a3 122 $self->_check_memoized_methods( $metaclass, \%options );
75f173e5 123
124 $metaclass->{'___original_class'} = blessed($metaclass);
125 bless $metaclass => $self->immutable_metaclass->name;
126}
c23184fc 127
75f173e5 128sub _inline_accessors {
129 my ( $self, $metaclass, $options ) = @_;
130
131 return unless $options->{inline_accessors};
132
133 foreach my $attr_name ( $metaclass->get_attribute_list ) {
134 $metaclass->get_attribute($attr_name)->install_accessors(1);
0ac992ee 135 }
75f173e5 136}
0ac992ee 137
75f173e5 138sub _inline_constructor {
139 my ( $self, $metaclass, $options ) = @_;
140
141 return unless $options->{inline_constructor};
142
2690a5c0 143 return
144 unless $options->{replace_constructor}
145 or !$metaclass->has_method( $options->{constructor_name} );
146
75f173e5 147 my $constructor_class = $options->{constructor_class}
148 || 'Class::MOP::Method::Constructor';
2690a5c0 149
75f173e5 150 $metaclass->add_method(
151 $options->{constructor_name},
152 $constructor_class->new(
153 options => $options,
154 metaclass => $metaclass,
155 is_inline => 1,
156 package_name => $metaclass->name,
157 name => $options->{constructor_name}
158 )
2690a5c0 159 );
160
75f173e5 161}
162
163sub _inline_destructor {
164 my ( $self, $metaclass, $options ) = @_;
165
166 return unless $options->{inline_destructor};
167
168 ( exists $options->{destructor_class} )
169 || confess "The 'inline_destructor' option is present, but "
170 . "no destructor class was specified";
171
172 my $destructor_class = $options->{destructor_class};
173
2690a5c0 174 return unless $destructor_class->is_needed($metaclass);
75f173e5 175
2690a5c0 176 my $destructor = $destructor_class->new(
177 options => $options,
178 metaclass => $metaclass,
179 package_name => $metaclass->name,
180 name => 'DESTROY'
181 );
182
183 return unless $destructor->is_needed;
184
185 $metaclass->add_method( 'DESTROY' => $destructor )
75f173e5 186}
187
bc79f8a3 188sub _check_memoized_methods {
75f173e5 189 my ( $self, $metaclass, $options ) = @_;
0ac992ee 190
c23184fc 191 my $memoized_methods = $self->options->{memoize};
75f173e5 192 foreach my $method_name ( keys %{$memoized_methods} ) {
c23184fc 193 my $type = $memoized_methods->{$method_name};
0ac992ee 194
75f173e5 195 ( $metaclass->can($method_name) )
196 || confess "Could not find the method '$method_name' in "
197 . $metaclass->name;
0ac992ee 198 }
c23184fc 199}
200
fd93a7b6 201sub create_methods_for_immutable_metaclass {
202 my $self = shift;
203
204 my %methods = %DEFAULT_METHODS;
205 my $metaclass = $self->metaclass;
206 my $meta = $metaclass->meta;
207
208 $methods{get_mutable_metaclass_name}
209 = sub { (shift)->{'___original_class'} };
210
211 $methods{immutable_transformer} = sub {$self};
212
213 return {
214 %DEFAULT_METHODS,
215 $self->_make_read_only_methods( $metaclass, $meta ),
216 $self->_make_uncallable_methods( $metaclass, $meta ),
217 $self->_make_memoized_methods( $metaclass, $meta ),
218 $self->_make_wrapped_methods( $metaclass, $meta ),
219 get_mutable_metaclass_name => sub { (shift)->{'___original_class'} },
220 immutable_transformer => sub {$self},
221 };
222}
223
224sub _make_read_only_methods {
225 my ( $self, $metaclass, $meta ) = @_;
226
227 my %methods;
228 foreach my $read_only_method ( @{ $self->options->{read_only} } ) {
229 my $method = $meta->find_method_by_name($read_only_method);
230
231 ( defined $method )
232 || confess "Could not find the method '$read_only_method' in "
233 . $metaclass->name;
234
235 $methods{$read_only_method} = sub {
236 confess "This method is read-only" if scalar @_ > 1;
237 goto &{ $method->body };
238 };
239 }
240
241 return %methods;
242}
243
244sub _make_uncallable_methods {
245 my ( $self, $metaclass, $meta ) = @_;
246
247 my %methods;
248 foreach my $cannot_call_method ( @{ $self->options->{cannot_call} } ) {
249 $methods{$cannot_call_method} = sub {
250 confess
251 "This method ($cannot_call_method) cannot be called on an immutable instance";
252 };
253 }
254
255 return %methods;
256}
257
258sub _make_memoized_methods {
259 my ( $self, $metaclass, $meta ) = @_;
260
261 my %methods;
262
263 my $memoized_methods = $self->options->{memoize};
264 foreach my $method_name ( keys %{$memoized_methods} ) {
265 my $type = $memoized_methods->{$method_name};
266 my $key = '___' . $method_name;
267 my $method = $meta->find_method_by_name($method_name);
268
269 if ( $type eq 'ARRAY' ) {
270 $methods{$method_name} = sub {
271 @{ $_[0]->{$key} } = $method->execute( $_[0] )
272 if !exists $_[0]->{$key};
273 return @{ $_[0]->{$key} };
274 };
275 }
276 elsif ( $type eq 'HASH' ) {
277 $methods{$method_name} = sub {
278 %{ $_[0]->{$key} } = $method->execute( $_[0] )
279 if !exists $_[0]->{$key};
280 return %{ $_[0]->{$key} };
281 };
282 }
283 elsif ( $type eq 'SCALAR' ) {
284 $methods{$method_name} = sub {
285 $_[0]->{$key} = $method->execute( $_[0] )
286 if !exists $_[0]->{$key};
287 return $_[0]->{$key};
288 };
289 }
290 }
291
292 return %methods;
293}
294
295sub _make_wrapped_methods {
296 my ( $self, $metaclass, $meta ) = @_;
297
298 my %methods;
299
300 my $wrapped_methods = $self->options->{wrapped};
301
302 foreach my $method_name ( keys %{$wrapped_methods} ) {
303 my $method = $meta->find_method_by_name($method_name);
304
305 ( defined $method )
306 || confess "Could not find the method '$method_name' in "
307 . $metaclass->name;
308
309 my $wrapper = $wrapped_methods->{$method_name};
310
311 $methods{$method_name} = sub { $wrapper->( $method, @_ ) };
312 }
313
314 return %methods;
315}
316
0ac992ee 317sub make_metaclass_mutable {
229910b5 318 my ($self, $immutable, $options) = @_;
319
320 my %options = %$options;
0ac992ee 321
322 my $original_class = $immutable->get_mutable_metaclass_name;
323 delete $immutable->{'___original_class'} ;
324 bless $immutable => $original_class;
325
326 my $memoized_methods = $self->options->{memoize};
327 foreach my $method_name (keys %{$memoized_methods}) {
328 my $type = $memoized_methods->{$method_name};
329
330 ($immutable->can($method_name))
331 || confess "Could not find the method '$method_name' in " . $immutable->name;
332 if ($type eq 'SCALAR' || $type eq 'ARRAY' || $type eq 'HASH' ) {
333 delete $immutable->{'___' . $method_name};
334 }
335 }
336
337 if ($options{inline_destructor} && $immutable->has_method('DESTROY')) {
338 $immutable->remove_method('DESTROY')
11b56828 339 if blessed($immutable->get_method('DESTROY')) eq $options{destructor_class};
0ac992ee 340 }
341
b817e248 342 # NOTE:
343 # 14:01 <@stevan> nah,. you shouldnt
344 # 14:01 <@stevan> they are just inlined
345 # 14:01 <@stevan> which is the default in Moose anyway
346 # 14:02 <@stevan> and adding new attributes will just DWIM
347 # 14:02 <@stevan> and you really cant change an attribute anyway
348 # if ($options{inline_accessors}) {
349 # foreach my $attr_name ($immutable->get_attribute_list) {
350 # my $attr = $immutable->get_attribute($attr_name);
351 # $attr->remove_accessors;
352 # $attr->install_accessors(0);
353 # }
354 # }
355
356 # 14:26 <@stevan> the only user of ::Method::Constructor is immutable
357 # 14:27 <@stevan> if someone uses it outside of immutable,.. they are either: mst or groditi
358 # 14:27 <@stevan> so I am not worried
11b56828 359 if ($options{inline_constructor} && $immutable->has_method($options{constructor_name})) {
0ac992ee 360 my $constructor_class = $options{constructor_class} || 'Class::MOP::Method::Constructor';
361 $immutable->remove_method( $options{constructor_name} )
11b56828 362 if blessed($immutable->get_method($options{constructor_name})) eq $constructor_class;
0ac992ee 363 }
364}
365
c23184fc 3661;
367
368__END__
369
370=pod
371
0ac992ee 372=head1 NAME
c23184fc 373
374Class::MOP::Immutable - A class to transform Class::MOP::Class metaclasses
375
376=head1 SYNOPSIS
377
96e38ba6 378 use Class::MOP::Immutable;
0ac992ee 379
96e38ba6 380 my $immutable_metaclass = Class::MOP::Immutable->new($metaclass, {
381 read_only => [qw/superclasses/],
382 cannot_call => [qw/
383 add_method
384 alias_method
385 remove_method
386 add_attribute
387 remove_attribute
388 add_package_symbol
0ac992ee 389 remove_package_symbol
96e38ba6 390 /],
391 memoize => {
392 class_precedence_list => 'ARRAY',
0ac992ee 393 compute_all_applicable_attributes => 'ARRAY',
394 get_meta_instance => 'SCALAR',
395 get_method_map => 'SCALAR',
96e38ba6 396 }
0ac992ee 397 });
96e38ba6 398
399 $immutable_metaclass->make_metaclass_immutable(@_)
400
c23184fc 401=head1 DESCRIPTION
402
0ac992ee 403This is basically a module for applying a transformation on a given
404metaclass. Current features include making methods read-only,
96e38ba6 405making methods un-callable and memoizing methods (in a type specific
0ac992ee 406way too).
96e38ba6 407
127d39a7 408This module is not for the feint of heart, it does some whacky things
409to the metaclass in order to make it immutable. If you are just curious,
410I suggest you turn back now, there is nothing to see here.
96e38ba6 411
c23184fc 412=head1 METHODS
413
414=over 4
415
96e38ba6 416=item B<new ($metaclass, \%options)>
417
0ac992ee 418Given a C<$metaclass> and a set of C<%options> this module will
419prepare an immutable version of the C<$metaclass>, which can then
420be applied to the C<$metaclass> using the C<make_metaclass_immutable>
96e38ba6 421method.
422
c23184fc 423=item B<options>
424
96e38ba6 425Returns the options HASH set in C<new>.
426
c23184fc 427=item B<metaclass>
428
96e38ba6 429Returns the metaclass set in C<new>.
430
c23184fc 431=item B<immutable_metaclass>
432
96e38ba6 433Returns the immutable metaclass created within C<new>.
434
c23184fc 435=back
436
437=over 4
438
439=item B<create_immutable_metaclass>
440
0ac992ee 441This will create the immutable version of the C<$metaclass>, but will
442not actually change the original metaclass.
96e38ba6 443
c23184fc 444=item B<create_methods_for_immutable_metaclass>
445
0ac992ee 446This will create all the methods for the immutable metaclass based
96e38ba6 447on the C<%options> passed into C<new>.
448
0ac992ee 449=item B<make_metaclass_immutable (%options)>
c23184fc 450
96e38ba6 451This will actually change the C<$metaclass> into the immutable version.
452
0ac992ee 453=item B<make_metaclass_mutable (%options)>
454
455This will change the C<$metaclass> into the mutable version by reversing
456the immutable process. C<%options> should be the same options that were
457given to make_metaclass_immutable.
458
c23184fc 459=back
460
461=head1 AUTHORS
462
463Stevan Little E<lt>stevan@iinteractive.comE<gt>
464
465=head1 COPYRIGHT AND LICENSE
466
69e3ab0a 467Copyright 2006-2008 by Infinity Interactive, Inc.
c23184fc 468
469L<http://www.iinteractive.com>
470
471This library is free software; you can redistribute it and/or modify
0ac992ee 472it under the same terms as Perl itself.
c23184fc 473
474=cut