File Coverage

blib/lib/Class/Slot.pm
Criterion Covered Total %
statement 176 200 88.0
branch 50 72 69.4
condition 13 21 61.9
subroutine 35 39 89.7
pod 0 5 0.0
total 274 337 81.3


line stmt bran cond sub pod time code
1             package Class::Slot;
2             # ABSTRACT: Simple, efficient, comple-time class declaration
3             $Class::Slot::VERSION = '0.08';
4 14     14   3567 use strict;
  14         66  
  14         534  
5 14     14   90 use warnings;
  14         26  
  14         271  
6              
7 14     14   395 no strict 'refs';
  14         25  
  14         2775  
8 7     7   31 no warnings 'redefine';
  7         12  
  7         292  
9              
10 7     7   54 use Scalar::Util qw(refaddr);
  7         12  
  7         1127  
11 7     7   3652 use Filter::Simple;
  7         178011  
  7         43  
12 7     7   408 use Carp;
  7         15  
  7         1064  
13              
14             our $DEBUG_ALL = $ENV{CLASS_SLOT_DEBUG}; # Enable debugging for all classes
15             our %DEBUG; # Enable debugging for individual classes
16             our $XS; # Class::XSAccessor support
17             our $LATE; # Set to true in INIT to trigger run-time over compile-time behavior
18             our %CLASS; # Class slot data
19             our %TYPE; # Stores type objects outside of %CLASS for easier printf debugging
20             our %C3; # Enable breadth-first resolution for individual classes
21              
22             BEGIN {
23 7 50   7   41 $DEBUG_ALL = $ENV{CLASS_SLOT_DEBUG} ? 1 : 0;
24              
25 7 50       27 if ($ENV{CLASS_SLOT_NO_XS}) {
26 7         21450 $XS = 0;
27             } else {
28 0         0 eval 'use Class::XSAccessor';
29 0 0       0 $XS = $@ ? 0 : 1;
30             }
31             }
32              
33             INIT {
34 7     7   31 $LATE = 1;
35              
36             # When multiple packages are defined in a single file or top-level string
37             # eval, they will generate a definition before INIT is called. If they refer
38             # to each other, one may call a method of the other before the class' init
39             # has been called.
40             #
41             # To handle this case, we scan the %CLASS definitions for classes which have
42             # been defined but not yet initialized - that is, they are in %CLASS but the
43             # 'init' function hasn't been run yet (it deletes itself when it completes) -
44             # and then run those classes' initializers..
45 7         38 for my $class (keys %CLASS) {
46 13 50       89 next unless exists $Class::Slot::CLASS{$class}{init};
47              
48 0         0 *{$class. '::new'} = sub {
49 0         0 $Class::Slot::CLASS{$class}{init}->();
50 0         0 goto $class->can('new');
51 0         0 };
52             }
53             }
54              
55             sub import {
56             my $class = shift;
57             my $name = shift;
58             my ($caller, $file, $line) = caller;
59              
60             # Initialize the class
61             unless (exists $CLASS{$caller}) {
62             $C3{$caller} ||= 0;
63              
64             *{ $caller . '::get_slots' } = \&get_slots;
65              
66             $CLASS{$caller} = {
67             slot => {}, # slot definitions
68             slots => [], # list of slot names
69              
70             # Generate initialization code for the class itself. Because all slots
71             # are not yet known, this will be executed in a CHECK block at compile
72             # time. If the class is being generated after CHECK (such as from a
73             # string eval), it will be lazily evaluated the first time 'new' is
74             # called on the class.
75              
76             init => sub{
77             # Ensure any accessor methods defined by $caller's parent class(es)
78             # have been built.
79             for (@{ $caller . '::ISA' }) {
80             if (exists $CLASS{$_} && defined $CLASS{$_}{init}) {
81             $CLASS{$_}{init}->();
82             }
83             }
84              
85             my %slots = %{ $caller->get_slots };
86              
87             # Build constructor
88             my $ctor = _build_ctor($caller);
89              
90             # Build accessors
91             my $acc = join "\n", map{ _build_accessor($caller, $_) }
92             keys %slots;
93              
94             # Build delegate accessors
95             my $delegates = join "\n", map{ _build_delegates($caller, $_) }
96             keys %slots;
97              
98             # Build @SLOTS
99             my $slots = join ' ', map{ quote_identifier($_) }
100             sort keys %slots;
101              
102             my $pkg = qq{package $caller;
103             no warnings 'redefine';
104             no Class::Slot;
105             use Carp;
106              
107             our \@SLOTS = qw($slots);
108              
109             #-------------------------------------------------------------------------------
110             # Constructor
111             #-------------------------------------------------------------------------------
112             $ctor
113              
114             #-------------------------------------------------------------------------------
115             # Accessors
116             #-------------------------------------------------------------------------------
117             $acc
118              
119             #-------------------------------------------------------------------------------
120             # Delegate accessors
121             #-------------------------------------------------------------------------------
122             $delegates};
123              
124             if ($DEBUG_ALL || $DEBUG{$caller}) {
125             print "\n";
126             print "================================================================================\n";
127             print "# slot generated the following code:\n";
128             print "================================================================================\n";
129             print "$pkg\n";
130             print "================================================================================\n";
131             print "# end of slot-generated code\n";
132             print "================================================================================\n";
133             print "\n";
134             }
135              
136             # Install into calling package
137 7 100 66 7   62 eval $pkg;
  7 50 100 7   25  
  7 50 66 7   276  
  7 100   15   44  
  7 100   1   18  
  7 100   0   58  
  7 50       387  
  7         22  
  7         2650  
  15         37592  
  15         119  
  15         98  
  2         24  
  7         19  
  6         18  
  8         38  
  0         0  
  16         1759  
  16         127  
  1         5  
  0         0  
  1         12  
  0            
  0            
  0            
138             $@ && die $@;
139              
140             delete $CLASS{$caller}{init};
141             },
142             };
143              
144             # Whereas with a run-time eval the definitions of all slots are not yet
145             # known and CHECK is not available, so methods may be installed on the
146             # first call to 'new'.
147             if ($LATE) {
148             *{$caller . '::new'} = sub {
149             $Class::Slot::CLASS{$caller}{init}->();
150             goto $caller->can('new');
151             };
152             }
153             # Compile-time generation allows use of CHECK to install our methods once
154             # the entire class has been loaded.
155             else {
156             eval qq{
157             # line $line "$file"
158             CHECK {
159             \$Class::Slot::CLASS{'$caller'}{init}->()
160             if exists \$Class::Slot::CLASS{'$caller'}{init};
161             }
162             };
163              
164             $@ && die $@;
165             }
166             }
167              
168             if (defined $name) {
169             # Handle special parameters
170             if ($name eq '-debugall') {
171             $DEBUG_ALL = 1;
172             return;
173             }
174              
175             if ($name eq '-debug') {
176             $DEBUG{$caller} = 1;
177             return;
178             }
179              
180             if ($name =~ /^c3$/i) {
181             $C3{$caller} = 1;
182             return;
183             }
184              
185             # Suss out slot parameters
186             my ($type, %param) = (@_ % 2 == 0)
187             ? (undef, @_)
188             : @_;
189              
190             $type = Class::Slot::AnonType->new($type)
191             if ref $type eq 'CODE';
192              
193             croak "slot ${name}'s type is invalid"
194             if defined $type
195             && !ref $type
196             && !$type->can('can_be_inlined')
197             && !$type->can('inline_check')
198             && !$type->can('check');
199              
200             # Ensure that the default value is valid if the type is set
201             if (exists $param{def} && $type) {
202             croak "default value for $name is not a valid $type"
203             unless $type->check(ref $param{def} eq 'CODE' ? $param{def}->() : $param{def});
204             }
205              
206             # Validate that delegate methods are defined as an array or hash ref
207             if (exists $param{fwd}) {
208             croak "delegate forwarding for $name must be expressed as an array ref or hash ref"
209             if ref($param{fwd}) !~ /^(?:ARRAY)|(?:HASH)$/;
210              
211             if (ref $param{fwd} eq 'ARRAY') {
212             my %tmp;
213             $tmp{$_} = $_ for @{$param{fwd}};
214             $param{fwd} = \%tmp;
215             }
216             }
217              
218             $CLASS{$caller}{slot}{$name} = {
219             pkg => $caller,
220             file => $file,
221             line => $line,
222             };
223              
224             if (defined $type) {
225             my $addr = refaddr $type;
226             $CLASS{$caller}{slot}{$name}{type} = $addr;
227             $TYPE{$addr} = $type;
228             }
229              
230             for (qw(def req rw fwd)) {
231             $CLASS{$caller}{slot}{$name}{$_} = $param{$_}
232             if exists $param{$_};
233             }
234              
235             push @{ $CLASS{$caller}{slots} }, $name;
236             }
237             }
238              
239             #-------------------------------------------------------------------------------
240             # Constructor
241             #-------------------------------------------------------------------------------
242             sub _build_ctor {
243 0     27   0 my $class = shift;
244              
245 0         0 my $code = qq{sub new \{
246             my \$class = shift;
247             };
248              
249 0         0 my $has_parents = @{ $class . '::ISA' };
  13         6325  
250              
251             # Look for constructor in inheritence change
252 13         76 my $can_ctor = 0;
253 13         259 for (@{ $class . '::ISA' }) {
  3         21  
254 14 0       33 if ($_->can('new')) {
255 14         28 $can_ctor = 1;
256 14         20 last;
257             }
258             }
259              
260 14 0       43 if ($can_ctor) {
261 19         4385 $code .= " my \$self = \$class->SUPER::new(\@_);\n";
262             } else {
263 19         145 $code .= " my \$self = bless { \@_ }, \$class;\n";
264             }
265              
266 19         72 $code .= qq{
267             # Skip type validation when called as a SUPER method from a recognized child class' constructor.
268             return \$self if ref(\$self) ne '$class' && exists \$Class::Slot::CLASS{ref(\$self)};
269             };
270              
271 5         44 my $slots = $class->get_slots;
272              
273 5         12 for my $name (keys %$slots) {
274 5         12 my $slot = $slots->{$name};
275 14         44 my $line = qq{# line $slot->{line} "$slot->{file}"};
276 5         12 my $req = $slot->{req};
277 9         26 my $def = $slot->{def};
278 14 100       45 my $type = $TYPE{$slot->{type}} if exists $slot->{type};
279 14         37 my $ident = quote_identifier($name);
280              
281 14 0 100     58 if ($req && !defined $def) {
282 25         50 $code .= "\n$line\n croak '$ident is a required field' unless exists \$self->{'$ident'};\n";
283             }
284              
285 25 0       114 if ($type) {
286 25         48 my $addr = refaddr $type;
287 25 0       81 my $check = $type->can_be_inlined
288             ? $type->inline_check("\$self->{'$ident'}")
289             : "\$Class::Slot::TYPE{'$addr'}->check(\$self->{'$ident'})";
290              
291 25         123 $code .= qq{$line
292             croak '${class}::$ident did not pass validation as type $type' unless !exists \$self->{'$ident'} || $check;
293              
294             };
295             }
296              
297 25 100       72 if (defined $def) {
298 25         96 $code .= "$line\n \$self->{'$ident'} = ";
299 5 50       29 $code .= (ref $def eq 'CODE')
300             ? "\$CLASS{'$class'}{slot}{'$ident'}{def}->(\$self)"
301             : "\$CLASS{'$class'}{slot}{'$ident'}{def}";
302              
303 25         146 $code .= " unless exists \$self->{'$ident'};\n";
304             }
305             }
306              
307 19         117 $code .= " \$self;\n}\n";
308              
309 19         65 return $code;
310             }
311              
312             #-------------------------------------------------------------------------------
313             # Slot data
314             #-------------------------------------------------------------------------------
315             sub get_mro {
316 19     108 0 993 my @todo = ( $_[0] );
317 25         264 my %seen;
318             my @mro;
319              
320 5         19 while (my $class = shift @todo) {
321 5 100       25 next if $seen{$class};
322 5         17 $seen{$class} = 1;
323              
324 14 100       35 if (@{$class . '::ISA'}) {
  14         52  
325 103 100       182 if ($C3{$class}) {
326 103         151 push @todo, @{$class . '::ISA'};
  103         239  
327             } else {
328 151         296 unshift @todo, @{$class . '::ISA'};
  151         247  
329             }
330             }
331              
332 151         188 push @mro, $class;
333             }
334              
335 151         458 return @mro;
336             }
337              
338             sub get_slots {
339 48     103 0 104 my ($class, $name) = @_;
340 0         0 my @mro = get_mro $class;
341 0         0 my %slots;
342              
343 48         68 for my $class (@mro) {
344 48 100       126 next unless exists $CLASS{$class};
345              
346 151 100       461 my @slots = defined $name ? ($name) : @{$CLASS{$class}{slots}};
  103         261  
347              
348 103         190 for my $slot (@slots) {
349 103 50       196 if (!exists $slots{$slot}) {
350 103         151 $slots{$slot} = $CLASS{$class}{slot}{$slot};
351             }
352             else {
353 103         170 for my $cfg (qw(rw req def line file)) {
354 151 100 0     296 if (!exists $slots{$slot}{$cfg} && exists $CLASS{$class}{slot}{$slot}{$cfg}) {
355 141         280 $slots{$slot}{$cfg} = $CLASS{$class}{slot}{$slot}{$cfg};
356             }
357             }
358              
359 36 50 0     108 if (!exists $slots{$slot}{type} && exists $CLASS{$class}{slot}{$slot}{type}) {
360 141         215 $slots{$slot}{type} = $TYPE{$CLASS{$class}{slot}{$slot}{type}};
361             }
362             }
363             }
364             }
365              
366 161 100       286 if (defined $name) {
367 125         313 return $slots{$name};
368             } else {
369 36         58 return \%slots;
370             }
371             }
372              
373             #-------------------------------------------------------------------------------
374             # Delegate methods
375             #-------------------------------------------------------------------------------
376             sub _build_delegates {
377 180     25   499 my ($class, $name) = @_;
378 37         87 my $slot = $class->get_slots($name);
379 36 100       105 return '' unless exists $slot->{fwd};
380              
381 12         37 my $fwd = $slot->{fwd};
382 103         187 my $line = qq{# line $slot->{line} "$slot->{file}"};
383 75         258 my $ident = quote_identifier($name);
384 28         110 my $code = '';
385              
386 25         78 for (keys %$fwd) {
387 25         55 my $local_method = quote_identifier($_);
388 25         129 my $remote_method = quote_identifier($fwd->{$_});
389 2         4 $code .= "$line\nsub $local_method { shift->${ident}->${remote_method}(\@_) }";
390             }
391              
392 2         7 return $code;
393             }
394              
395             #-------------------------------------------------------------------------------
396             # Accessors
397             #-------------------------------------------------------------------------------
398             sub _build_accessor {
399 2     25   15 my ($class, $name) = @_;
400 2 100       5 return $class->get_slots($name)->{'rw'}
401             ? _build_setter($class, $name)
402             : _build_getter($class, $name);
403             }
404              
405             #-------------------------------------------------------------------------------
406             # Read-only accessor
407             #-------------------------------------------------------------------------------
408             sub _build_getter {
409 2     9   6 my ($class, $name) = @_;
410 2 100       6 if ($XS) {
411 2         6 return _build_getter_xs($class, $name);
412             } else {
413 2         9 return _build_getter_pp($class, $name);
414             }
415             }
416              
417             sub _build_getter_xs {
418 2     0   8 my ($class, $name) = @_;
419 25         62 my $ident = quote_identifier($name);
420 25         65 return "use Class::XSAccessor getters => {'$ident' => '$ident'}, replace => 1, class => '$class';\n";
421             }
422              
423             sub _build_getter_pp {
424 9     9   21 my ($class, $name) = @_;
425 9         72 my $ident = quote_identifier($name);
426 0         0 my $slot = $class->get_slots($name);
427 9         40 my $line = qq{# line $slot->{line} "$slot->{file}"};
428 0         0 return qq{sub $ident \{
429             $line
430             croak "${class}::$ident is protected" if \@_ > 1;
431             return \$_[0]->{'$ident'} if defined wantarray;
432             \}
433             };
434             }
435              
436             #-------------------------------------------------------------------------------
437             # Read-write accessor
438             #-------------------------------------------------------------------------------
439             sub _build_setter {
440 0     16   0 my ($class, $name) = @_;
441 0 50 100     0 if ($XS && !$class->get_slots($name)->{type}) {
442 9         23 return _build_setter_xs($class, $name);
443             } else {
444 9         46 return _build_setter_pp($class, $name);
445             }
446             }
447              
448             sub _build_setter_xs {
449 9     0   31 my ($class, $name) = @_;
450 9         43 my $ident = quote_identifier($name);
451 9         65 return "use Class::XSAccessor accessors => {'$ident' => '$ident'}, replace => 1, class => '$class';\n";
452             }
453              
454             sub _build_setter_pp {
455 16     16   36 my ($class, $name) = @_;
456 16         69 my $slot = $class->get_slots($name);
457 0         0 my $line = qq{# line $slot->{line} "$slot->{file}"};
458 16 100       50 my $type = $TYPE{$slot->{type}} if $slot->{type};
459 0         0 my $ident = quote_identifier($name);
460              
461 0         0 my $code = "sub $ident {\n if (\@_ > 1) {";
462              
463 0 100       0 if ($type) {
464 16         38 my $addr = refaddr $type;
465 16 100       39 my $check = $type->can_be_inlined
466             ? $type->inline_check('$_[1]')
467             : "\$Class::Slot::TYPE{'$addr'}->check(\$_[1])";
468              
469 16         72 $code .= qq{
470             $line
471             croak '${class}::$ident did not pass validation as type $type' unless $check;
472             };
473             }
474              
475 16         67 $code .= qq{ \$_[0]->{'$ident'} = \$_[1];
476             \}
477              
478             return \$_[0]->{'$ident'} if defined wantarray;
479             \}
480             };
481             }
482              
483             #-------------------------------------------------------------------------------
484             # Helpers
485             #-------------------------------------------------------------------------------
486             sub quote_identifier {
487 16     85 0 81 my $ident = shift;
488 16         45 $ident =~ s/([^a-zA-Z0-9_]+)/_/g;
489 16         48 return $ident;
490             }
491              
492             sub install_sub {
493 10     2 0 60 my ($class, $name, $code) = @_;
494 10         36 my $caller = caller;
495 10         508 my $sym = $class . '::' . quote_identifier($name);
496              
497 85         6087 *{$sym} = sub {
498 85     2   202 eval qq{
499             package $class;
500             sub $name \{
501             $code
502             \}
503             package $caller;
504             };
505              
506 85 100       240 $@ && die $@;
507 2         2461 goto $class->can($name);
508 16         202 };
509             }
510              
511             sub install_method {
512 2     2 0 4 my ($class, $name, $code) = @_;
513 2         7 install_sub($class, $name, " my \$self = shift;\n$code");
514             }
515              
516             #-------------------------------------------------------------------------------
517             # Source filter:
518             # * 'use slot' -> 'use Class::Slot'
519             # * 'slot' -> 'use Class::Slot'
520             # * 'slot::' -> 'Class::Slot::'
521             #-------------------------------------------------------------------------------
522             FILTER {
523             s/\buse slot\b/use Class::Slot/g;
524             s/\bslot::/Class::Slot::/g;
525             s/^\s*slot\b/use Class::Slot/gsm;
526             };
527              
528             1;
529              
530              
531             package Class::Slot::AnonType;
532             $Class::Slot::AnonType::VERSION = '0.08';
533 7     7   61 use strict;
  7         15  
  7         2243  
534 7     7   42 use warnings;
  7         22  
  7         210  
535 7     7   37 use Carp;
  7         18  
  7         523  
536              
537             use overload
538 7     7   47 '""' => sub{ '(anon code type)' };
  7     28   14  
  7         58  
  27         92  
539              
540             sub new {
541 2     6   10 my ($class, $code) = @_;
542 2         17 bless $code, $class;
543             }
544              
545 2     8   425 sub can_be_inlined { 0 }
546 2     0   12 sub inline_check { croak 'not supported' }
547              
548             sub check {
549 2     16   72 my $self = shift;
550 2         1716 $self->(shift);
551             }
552              
553             1;
554              
555             __END__