File Coverage

blib/lib/MooX/XSConstructor.pm
Criterion Covered Total %
statement 100 110 90.9
branch 21 30 70.0
condition 11 15 73.3
subroutine 15 15 100.0
pod 3 3 100.0
total 150 173 86.7


line stmt bran cond sub pod time code
1 4     4   437715 use 5.008001;
  4         17  
2 4     4   19 use strict;
  4         8  
  4         110  
3 4     4   17 use warnings;
  4         7  
  4         346  
4              
5             package MooX::XSConstructor;
6              
7             our $AUTHORITY = 'cpan:TOBYINK';
8             our $VERSION = '0.003003';
9              
10 4     4   2160 use Class::Method::Modifiers 'install_modifier';
  4         6785  
  4         316  
11 4     4   25 use Moo 2.004000 ();
  4         68  
  4         69  
12 4     4   18 use Moo::Object ();
  4         5  
  4         58  
13 4     4   1700 use Hook::AfterRuntime;
  4         12465  
  4         3796  
14              
15             # Options that either XSCON can handle, or have no effect on the
16             # constructor at all.
17             my $safe_spec = qr/\A(
18             index |
19             is | reader | writer | accessor | predicate | clearer |
20             handles | handles_via |
21             init_arg | required |
22             isa | coerce |
23             builder | default | lazy |
24             trigger |
25             weak_ref |
26             documentation
27             )\z/x;
28              
29             # Options that have no effect on the constructor at all, but need
30             # to be deleted so they don't confuse XSCON.
31             my $delete_from_spec = qr/\A(
32             index |
33             is | reader | writer | accessor | predicate | clearer |
34             handles | handles_via |
35             documentation
36             )\z/x;
37              
38             sub is_suitable_class {
39 12     12 1 29 my ( $self, $klass ) = @_;
40            
41 12 100       59 return unless Moo->is_class( $klass );
42            
43 11         111 my $constructor_maker = Moo->_constructor_maker_for( $klass );
44 11         32469 do {
45             # If the class's constructor generator is Method::Generate::Constructor,
46             # then we can predict its behaviour. If it's something unknown, we
47             # don't know what it might do internally, so cannot safely replace
48             # it with XSCON.
49 11         38 my @allowed_constructor_makers = (
50             'Method::Generate::Constructor',
51             );
52            
53             # This is one trait that might be applied to the constructor maker
54             # which we *know* we are able to handle.
55 11 50       52 if ( $INC{'MooX/StrictConstructor/Role/Constructor/Late.pm'} ) {
56 0         0 require Role::Tiny;
57 0         0 require Method::Generate::Constructor;
58 0         0 my $trait = 'MooX::StrictConstructor::Role::Constructor::Late';
59             push @allowed_constructor_makers, map {
60 0         0 Role::Tiny->create_class_with_roles( $_, $trait );
  0         0  
61             } @allowed_constructor_makers;
62             }
63              
64 11         27 my $maker_class = ref $constructor_maker;
65 11 50       26 return unless grep { $maker_class eq $_ } @allowed_constructor_makers;
  11         45  
66             };
67            
68             # Preliminary sanity check for accessor maker, because it is
69             # used by the constructor genmerator to generate attribute set
70             # statements. Just check it's theoretically sound here, but we
71             # still need to check it for each attribute.
72 11         51 my $accessor_maker = Moo->_accessor_maker_for( $klass );
73 11         72 do {
74 11         49 my $generated = $accessor_maker->_generate_core_set( q{$XXX}, 'yyy', {}, q{"zzz"} );
75 11         181 my $expected = q{$XXX->{"yyy"} = "zzz"};
76 11 50       32 return if $generated ne $expected;
77             };
78            
79             # Loop through attributes...
80 11         17 my %spec = %{ $constructor_maker->all_attribute_specs };
  11         36  
81 11         84 my @attributes = sort { $spec{$a}{index} <=> $spec{$b}{index} } keys %spec;
  9         26  
82 11         29 for my $attr (@attributes) {
83             # Check they don't have any options we don't understand.
84 15 50       21 if (my @unsafe = grep { $_ !~ $safe_spec } keys %{ $spec{$attr} }) {
  58         231  
  15         37  
85 0         0 return;
86             }
87            
88             # Check that the accessor maker generates a predictable
89             # attribute set statement for this attribute.
90 15         72 require B;
91 15         42 my $generated = $accessor_maker->_generate_core_set( q{$XXX}, $attr, $spec{$attr}, q{"zzz"} );
92 15         197 my $expected = sprintf q{$XXX->{%s} = "zzz"}, B::perlstring($attr);
93 15 50       45 return if $generated ne $expected;
94             }
95            
96             # If we got this far, we're good.
97 11         46 return "yay!";
98             }
99              
100             # Called by the import method.
101             sub setup_for {
102 7     7 1 21 my ( $self, $klass ) = @_;
103            
104 7 50       29 return unless $self->is_suitable_class( $klass );
105            
106             # Get attribute specs
107 7         26 my $maker = Moo->_constructor_maker_for( $klass );
108 7         44 my %spec = %{ $maker->all_attribute_specs };
  7         19  
109            
110             # Transform it into arguments which XSCON can handle
111             my @args =
112             map {
113 10         20 my $slot = $_;
114 10         16 my %xs_spec = %{ $spec{$slot} };
  10         37  
115 10         29 for my $k ( keys %xs_spec ) {
116 37 100       163 delete $xs_spec{$k} if $k =~ $delete_from_spec;
117             }
118 10 50       27 if ( $xs_spec{lazy} ) {
119 0         0 delete $xs_spec{$_} for qw/ lazy builder default /;
120             }
121 10         42 ( $slot => \%xs_spec );
122             }
123 7         43 sort { $spec{$a}{index} <=> $spec{$b}{index} }
  6         16  
124             keys %spec;
125            
126 7 50       126 $maker->DOES( 'MooX::StrictConstructor::Role::Constructor::Late' )
127             and push @args, '!!';
128              
129             # Keep track of old constructor, just in case.
130 7         37 my $old = $klass->can( 'new' );
131            
132             # Call XSCON to replace the existing constructor method
133 7         20 my $ok = eval {
134 7         2741 require Class::XSConstructor;
135 7         474161 Class::XSConstructor->VERSION( 0.018001 );
136 7         25 local $Class::XSConstructor::REDEFINE = !!1;
137 7         42 Class::XSConstructor->import( [ $klass, 'new' ], @args );
138 7         7672 1;
139             };
140            
141            
142 7 50 33     42 if ( $ok and my $meta = Class::XSConstructor::get_metadata( $klass ) ) {
143 7 100 100     174 if ( defined $meta->{foreignclass}
      100        
144             and ( $meta->{foreignclass} eq 'Moo::Object' or $self->is_suitable_class( $meta->{foreignclass} ) ) ) {
145 5         31 delete $meta->{$_} for qw/
146             foreignbuildall
147             foreignconstructor
148             foreignclass
149             foreignbuildargs
150             /;
151             }
152 7 100 66     48 if ( $meta->{buildargs} and $meta->{buildargs} == \&Moo::Object::BUILDARGS ) {
153 6         15 $meta->{has_standard_buildargs} = !!1;
154 6         15 delete $meta->{buildargs};
155             }
156 7         22 $klass->XSCON_CLEAR_CONSTRUCTOR_CACHE;
157             }
158             else {
159             # If there was a failure, restore old constructor.
160 4     4   42 no strict 'refs';
  4         10  
  4         147  
161 4     4   17 no warnings 'redefine';
  4         10  
  4         1660  
162 0         0 *{"${klass}::new"} = $old;
  0         0  
163 0         0 return;
164             }
165            
166 7 100       42 if ( $klass->can('DEMOLISH') ) {
167 1         417 require Class::XSDestructor;
168 1         380 local $Class::XSDestructor::REDEFINE = !!1;
169 1         5 Class::XSDestructor->import( [ $klass, 'DESTROY' ] );
170             }
171            
172 7         165 return $klass;
173             }
174              
175             sub import {
176 7     7   2160 my $self = shift;
177 7         15 my $caller = caller;
178 7 100 66     33 if ( @_ and $_[0] eq '-wrapconstructor' ) {
179             after_runtime {
180             install_modifier $caller => around => new => sub {
181 1         237929 my $pp_constructor = shift;
182 1         24 my $object = $pp_constructor->( @_ );
183 1         2002 $self->setup_for( $caller );
184 1         19 return $object;
185 1     1   18 };
186 1         6 };
187             }
188             else {
189             after_runtime {
190 6     6   460514 $self->setup_for( $caller )
191 6         40 };
192             }
193             }
194              
195             sub is_xs {
196 4     4 1 20 require B;
197 4         31 !! B::svref_2object( shift )->XSUB;
198             }
199              
200             __PACKAGE__
201             __END__