File Coverage

lib/MooseX/Extended/Core.pm
Criterion Covered Total %
statement 265 294 90.1
branch 71 94 75.5
condition 40 64 62.5
subroutine 43 47 91.4
pod 0 2 0.0
total 419 501 83.6


line stmt bran cond sub pod time code
1              
2             # ABSTRACT: Internal module for MooseX::Extended
3              
4             use v5.20.0;
5 19     19   188213 use warnings;
  19         70  
6 19     19   88 use parent 'Exporter';
  19         38  
  19         489  
7 19     19   92 use Moose::Util qw(
  19         59  
  19         149  
8 19         136 add_method_modifier
9             throw_exception
10             );
11 19     19   2373 use MooseX::Extended::Types qw(
  19         239008  
12 19         216 ArrayRef
13             Bool
14             Dict
15             Enum
16             NonEmptyStr
17             Optional
18             Str
19             Undef
20             compile_named
21             );
22 19     19   14355 use Module::Load 'load';
  19         69  
23 19     19   103289 use feature qw(signatures postderef);
  19         174  
  19         186  
24 19     19   1329 no warnings qw(experimental::signatures experimental::postderef);
  19         40  
  19         2164  
25 19     19   119  
  19         35  
  19         2764  
26             use Storable 'dclone';
27 19     19   11594 use Ref::Util qw(
  19         52492  
  19         1420  
28 19         950 is_plain_arrayref
29             is_coderef
30             );
31 19     19   1472 use Carp 'croak';
  19         1559  
32 19     19   105 #
  19         36  
  19         1438  
33              
34             our $VERSION = '0.33';
35              
36             our @EXPORT_OK = qw(
37             _assert_import_list_is_valid
38             _debug
39             _disabled_warnings
40             _enabled_features
41             _our_import
42             _our_init_meta
43             field
44             param
45             );
46              
47             # Core's use feature 'try' only supports 'finally' since 5.35.8
48             use constant HAVE_FEATURE_TRY => $] >= 5.035008;
49 19     19   113  
  19         33  
  19         42472  
50              
51 96     96   7855 warnings::register_categories(
52 96     96   5319 'MooseX::Extended::naked_fields',
53             );
54              
55             # Should this be in the metaclass? It feels like it should, but
56             # the MOP really doesn't support these edge cases.
57             my %CONFIG_FOR;
58              
59             return $CONFIG_FOR{$package};
60             }
61              
62 0     0   0  
  0         0  
  0         0  
63 0         0 # don't use signatures for this import because we need @_ later. @_ is
64             # intended to be removed for subs with signature
65             my ( $class, $import, $target_class ) = @_;
66              
67             # Moose::Exporter uses Sub::Exporter to handle exporting, so it accepts an
68             # { into =>> $target_class } to say where we're exporting this to. This is
69             # used by our ::Custom modules to let people define their own versions
70 69     69   208 @_ = ( $class, { into => $target_class } ); # anything else and $import blows up
71             goto $import;
72             }
73              
74             # asserts the import list is valid, rewrites the excludes and includes from
75 69         227 # arrays to hashes (if ( $args{excludes}{$feature} ) ...) and returns the
76 69         282 # target package that this code will be applied to. Yeah, it does too much.
77             my ( $class, $args ) = @_;
78              
79             foreach my $features (qw/types excludes/) {
80             if ( exists $args->{$features} && !ref $args->{$features} ) {
81             $args->{$features} = [ $args->{$features} ];
82             }
83 71     71   161 }
84             if ( my $includes = $args->{includes} ) {
85 71         158 if ( !ref $includes ) {
86 142 100 100     523 $args->{includes} = { $includes => undef };
87 4         13 }
88             elsif ( is_plain_arrayref($includes) ) {
89             $args->{includes} = { map { $_ => undef } $includes->@* };
90 71 100       207 }
91 4 100       17 else {
    100          
92 1         5 # let anything else just fail in type checking
93             }
94             }
95 2         4  
  2         8  
96             $args->{call_level} //= 0;
97             my ( $package, $filename, $line ) = caller( $args->{call_level} + 1 );
98             my $target_class = $args->{for_class} // $package;
99              
100             state $check = {
101             class => compile_named( _default_import_list(), _class_excludes() ),
102 71   100     332 role => compile_named( _default_import_list(), _role_excludes() )
103 71         286 };
104 71   66     729 eval {
105             $check->{ $args->{_import_type} }->( $args->%* );
106 71         148 1;
107             } or do {
108              
109             # Not sure what's happening, but if we don't use the eval to trap the
110             # error, it gets swallowed and we simply get:
111 71         501 #
112 69         3577 # BEGIN failed--compilation aborted at ...
113 71 100       335655 #
114             # Also, don't use $target_class here because if it's different from
115             # $package, the filename and line number won't match
116             my $error = $@;
117             Carp::carp(<<"END");
118             Error: Invalid import list to $class.
119             Package: $package
120             Filename: $filename
121             Line: $line
122 2         662 Details: $error
123 2         39 END
124             throw_exception(
125             'InvalidImportList',
126             class_name => $package,
127             moosex_extended_type => __PACKAGE__,
128             line_number => $line,
129             messsage => $error,
130 2         917 );
131             };
132              
133             # remap the array to a hash for easy lookup
134             foreach my $features (qw/excludes/) {
135             $args->{$features} = { map { $_ => 1 } $args->{$features}->@* };
136             }
137              
138             $CONFIG_FOR{$target_class} = $args;
139             return $target_class;
140 69         164 }
141 69         255  
  11         40  
142             my $for_class = $params{for_class};
143             my $config = $CONFIG_FOR{$for_class};
144 69         168  
145 69         232 if ( $config->{debug} ) {
146             $MooseX::Extended::Debug = $config->{debug};
147             }
148 69     69   140  
  69         112  
  69         111  
  69         155  
  69         99  
149 69         149 if ( _should_debug() ) {
150 69         134 foreach my $feature (qw/includes excludes/) {
151             if ( exists $config->{$feature} ) {
152 69 50       216 foreach my $category ( sort keys $config->{$feature}->%* ) {
153 0         0 _debug("$for_class $feature '$category'");
154             }
155             }
156 69 50       188 }
157 0         0 }
158 0 0       0  
159 0         0 $apply_default_features->( $config, $for_class, \%params );
160 0         0 _apply_optional_features( $config, $for_class );
161             }
162              
163             return (
164             with_meta => [ 'field', 'param' ],
165             install => [qw/unimport/],
166 69         390 also => ['Moose'],
167 69         6719 );
168             }
169              
170 0     0   0 return (
  0         0  
171             with_meta => [ 'field', 'param' ],
172 0         0 );
173             }
174              
175             return (
176             excludes => Optional [
177             ArrayRef [
178 0     0   0 Enum [
  0         0  
179             qw/
180 0         0 WarnOnConflict
181             autoclean
182             carp
183             true
184 18     18   88271 field
  18         47  
185             param
186 18         171 /
187             ]
188             ]
189             ]
190             );
191             }
192              
193             return (
194             excludes => Optional [
195             ArrayRef [
196             Enum [
197             qw/
198             StrictConstructor
199             autoclean
200             c3
201             carp
202             immutable
203 18     18   157154 true
  18         40  
204             field
205 18         144 param
206             /
207             ]
208             ]
209             ]
210             );
211             }
212              
213             return (
214             call_level => Optional [ Enum [ 1, 0 ] ],
215             debug => Optional [Bool],
216             for_class => Optional [NonEmptyStr],
217             types => Optional [ ArrayRef [NonEmptyStr] ],
218             _import_type => Enum [qw/class role/],
219             _caller_eval => Bool, # https://github.com/Ovid/moosex-extended/pull/34
220             includes => Optional [ Dict [ map { $_ => Optional [ Undef | ArrayRef ] } qw/ multi async try method / ] ],
221             );
222             }
223              
224 36     36   1101339 if ($requested) {
  36         69  
225             return $requested->@*;
226             }
227             elsif ($defaults) {
228             return $defaults->@*;
229             }
230             return;
231             }
232 36         224  
  144         692599  
233             my $includes = $config->{includes} or return;
234              
235             state $requirements_for = {
236 5     5   9 multi => {
  5         6  
  5         6  
  5         7  
237 5 100       18 version => v5.26.0,
    100          
238 1         8 import => undef,
239             module => 'Syntax::Keyword::MultiSub',
240             },
241 1         5 async => {
242             version => v5.26.0,
243 3         17 import => undef,
244             module => 'Future::AsyncAwait',
245             },
246 69     69   175 method => {
  69         109  
  69         94  
  69         84  
247 69 100       376 version => v5.0.0,
248             import => ['method'],
249 3         5 module => 'Function::Parameters',
250             },
251             try => {
252             version => v5.24.0,
253             import => undef,
254             module => 'Syntax::Keyword::Try',
255             skip => sub ($for_class) {
256             if (HAVE_FEATURE_TRY) {
257             feature->import::into($for_class);
258             warnings->unimport('experimental::try');
259             return 1;
260             }
261             return;
262             },
263             }
264             };
265             FEATURE: foreach my $feature ( keys $includes->%* ) {
266             my $required = $requirements_for->{$feature} or croak("PANIC: we have requested a non-existent feature: $feature");
267             if ( $^V && $^V lt $required->{version} ) {
268             croak("Feature '$feature' not supported in Perl version less than $required->{version}. You have $^V");
269 3     3   3 }
  3         5  
270 3         6  
271             # don't trap the error. Let it bubble up.
272             if ( my $skip = $required->{skip} ) {
273             next FEATURE if $skip->($for_class);
274             }
275 3         6 load $required->{module};
276             $required->{module}->import::into( $for_class, _with_imports( $includes->{$feature}, $required->{import} ) );
277             }
278 4         54 }
279 4         15  
280 5 50       611 $opt_for{is} //= 'ro';
281 5 50 33     167 $opt_for{required} //= 1;
282 0         0 $opt_for{_call_level} //= 1;
283              
284             # "has [@attributes]" versus "has $attribute"
285             foreach my $attr ( is_plain_arrayref($name) ? @$name : $name ) {
286 5 100       29 my %options = %opt_for; # copy each time to avoid overwriting
287 3 50       9 $options{init_arg} //= $attr;
288              
289 5         22 # in case they're inheriting an attribute
290 5         4001 $options{init_arg} =~ s/\A\+//;
291             _add_attribute( 'param', $meta, $attr, %options );
292             }
293             }
294 32     32 0 149043  
  32         69  
  32         53  
  32         98  
  32         47  
295 32   100     193 $opt_for{is} //= 'ro';
296 32   100     139 $opt_for{_call_level} //= 1;
297 32   50     156  
298             # "has [@attributes]" versus "has $attribute"
299             foreach my $attr ( is_plain_arrayref($name) ? @$name : $name ) {
300 32 100       124 my %options = %opt_for; # copy each time to avoid overwriting
301 36         18067 if ( defined( my $init_arg = $options{init_arg} ) ) {
302 36   66     170 $init_arg =~ /\A_/ or throw_exception(
303             'InvalidAttributeDefinition',
304             attribute_name => $name,
305 36         97 class_name => $meta->name,
306 36         125 messsage => "A defined 'field.init_arg' must begin with an underscore: '$init_arg'",
307             );
308             }
309              
310 16     16 0 44255 $options{init_arg} //= undef;
  16         40  
  16         38  
  16         56  
  16         26  
311 16   100     117 if ( $options{builder} || $options{default} ) {
312 16   50     101 $options{lazy} //= 1;
313             }
314              
315 16 50       74 _add_attribute( 'field', $meta, $attr, %options );
316 16         71 }
317 16 100       102 }
318 2 100       13  
319             _debug("Finalizing options for '$attr_type $name'");
320              
321             # we use the $name to generate the other methods names. However,
322             # $orig_name is used to set the actual field name. This is because
323             # Moose allows `has '+x' => ( writer => 'set_x' );` to inherit an
324             # attribute from a parent class and only change the desired attribute
325             # options.
326 15   100     80 my $orig_name = $name;
327 15 100 100     91 $name =~ s/\A\+//;
328 12   100     66 unless ( _is_valid_method_name($name) ) {
329             throw_exception(
330             'InvalidAttributeDefinition',
331 15         114 attribute_name => $orig_name,
332             class_name => $meta->name,
333             messsage => "Illegal attribute name, '$name'",
334             );
335 51     51   90 }
  51         78  
  51         75  
  51         70  
  51         124  
  51         68  
336 51         237  
337             state $shortcut_for = {
338             predicate => sub ($value) {"has_$value"},
339             clearer => sub ($value) {"clear_$value"},
340             builder => sub ($value) {"_build_$value"},
341             writer => sub ($value) {"set_$value"},
342             reader => sub ($value) {"get_$value"},
343 51         95 };
344 51         115  
345 51 100       131 if ( is_coderef( $opt_for{builder} ) ) {
346 1         6 my $builder_code = $opt_for{builder};
347             my $builder_name = $shortcut_for->{builder}->($name);
348             if ( _is_valid_method_name($builder_name) ) {
349             $meta->add_method( $builder_name => $builder_code );
350             $opt_for{builder} = $builder_name;
351             }
352             }
353              
354 4         7 OPTION: foreach my $option ( keys $shortcut_for->%* ) {
355 4     4   8 next unless exists $opt_for{$option};
  4         14  
  4         10  
356 6     6   9 no warnings 'numeric'; ## no critic (TestingAndDebugging::ProhibitNoWarning)
  6         14  
  6         12  
  6         11  
357 3     3   6 if ( 1 == length( $opt_for{$option} ) && 1 == $opt_for{$option} ) {
  3         10  
  3         4  
  3         7  
358 9     9   13 my $option_name = $shortcut_for->{$option}->($name);
  9         28  
  9         27  
  9         21  
359 0     0   0 $opt_for{$option} = $option_name;
  0         0  
  0         0  
  0         0  
360 50         366 }
361             unless ( _is_valid_method_name( $opt_for{$option} ) ) {
362 50 100       279 throw_exception(
363 2         5 'InvalidAttributeDefinition',
364 2         9 attribute_name => $orig_name,
365 2 50       6 class_name => $meta->name,
366 2         11 messsage => "Attribute '$orig_name' has an invalid option name, $option => '$opt_for{$option}'",
367 2         84 );
368             }
369             }
370              
371 50         181 if ( 'rwp' eq $opt_for{is} ) {
372 250 100       464 $opt_for{writer} = "_set_$name";
373 19     19   179 }
  19         63  
  19         9368  
374 23 100 66     144  
375 20         65 if ( exists $opt_for{writer} && defined $opt_for{writer} ) {
376 20         48 $opt_for{is} = 'rw';
377             }
378 23 100       68  
379 1         12 %opt_for = _maybe_add_cloning_method( $meta, $name, %opt_for );
380              
381             if ( not exists $opt_for{accessor}
382             and not exists $opt_for{writer}
383             and not exists $opt_for{default}
384             and not exists $opt_for{builder}
385             and not defined $opt_for{init_arg}
386             and $opt_for{is} eq 'ro' )
387             {
388 49 100       150  
389 2         7 my $call_level = 1 + $opt_for{_call_level};
390             my ( undef, $filename, $line ) = caller($call_level);
391             Carp::carp("$attr_type '$name' is read-only and has no init_arg or default, defined at $filename line $line\n")
392 49 100 66     197 if $] ge '5.028'
393 11         30 and warnings::enabled_at_level( 'MooseX::Extended::naked_fields', $call_level );
394             }
395              
396 49         195 delete $opt_for{_call_level};
397             _debug( "Setting $attr_type, '$orig_name'", \%opt_for );
398 49 50 66     448 $meta->add_attribute( $orig_name, %opt_for );
      33        
399             }
400              
401             return if ref $name;
402             return $name =~ qr/\A[a-z_]\w*\z/ai;
403             }
404              
405             return %opt_for unless my $clone = delete $opt_for{clone};
406 0         0  
407 0         0 no warnings 'numeric'; ## no critic (TestingAndDebugging::ProhibitNoWarning)
408 0 0 0     0  
409             my ( $use_dclone, $use_coderef, $use_method );
410             if ( 1 == length($clone) && 1 == $clone ) {
411             $use_dclone = 1;
412             }
413 49         92 elsif ( _is_valid_method_name($clone) ) {
414 49         194 $use_method = 1;
415 49         314 }
416             elsif ( is_coderef($clone) ) {
417             $use_coderef = 1;
418 78     78   108 }
  78         104  
  78         98  
419 78 100       160 else {
420 77         719 throw_exception(
421             'InvalidAttributeDefinition',
422             attribute_name => $name,
423 49     49   72 class_name => $meta->name,
  49         76  
  49         72  
  49         126  
  49         74  
424 49 100       288 messsage => "Attribute '$name' has an invalid option value, clone => '$clone'",
425             );
426 19     19   140 }
  19         38  
  19         15216  
427              
428 4         8 # here be dragons ...
429 4 100 66     17 _debug("Adding cloning for $name");
    100          
    50          
430 2         4 my $reader = delete( $opt_for{reader} ) // $name;
431             my $writer = delete( $opt_for{writer} ) // $reader;
432             my $is = $opt_for{is};
433 1         2 $opt_for{is} = 'bare';
434              
435             my $reader_method = sub ($self) {
436 1         3 _debug("Calling reader method for $name");
437             my $attr = $meta->get_attribute($name);
438             my $value = $attr->get_value($self);
439 0         0 return $value unless ref $value;
440             return
441             $use_dclone ? dclone($value)
442             : $use_method || $use_coderef ? $self->$clone( $name, $value )
443             : croak("PANIC: this should never happen. Do not know how to clone '$name'");
444             };
445              
446             my $writer_method = sub ( $self, $new_value ) {
447             _debug("Calling writer method for $name");
448 4         13 my $attr = $meta->get_attribute($name);
449 4   33     12 $new_value
450 4   66     12 = !ref $new_value ? $new_value
451 4         7 : $use_dclone ? dclone($new_value)
452 4         5 : $use_method || $use_coderef ? $self->$clone( $name, $new_value )
453             : croak("PANIC: this should never happen. Do not know how to clone '$name'");
454 24     24   25 $new_value = ref $new_value ? dclone($new_value) : $new_value;
  24     24   3084  
  24     24   27  
        24      
455 24         66 $attr->set_value( $self, $new_value );
456 24         71 return $new_value;
457 24         176 };
458 24 50       3466  
459             # this fixes a bug where we could set the value in the constructor
460 24 50 66     517 # but it would remain a reference to the original data, so we could do
    100          
461             # this:
462             #
463 4         16 # my $date = DateTime->now;
464             # my $object = Some::Classs->new( created => $date );
465 2     3   4 #
  3         994  
  2         4  
  2         2  
466 2         13 # Any subsequent code calling $object->created was getting a reference to
467 2         7 # $date, so any changes to $date would be propagated to all instances
468 2 0 0     75 $meta->add_before_method_modifier(
    50          
    50          
469             BUILD => sub ( $self, @ ) {
470             my $attr = $meta->get_attribute($name);
471              
472             # before BUILD is even called, let's make sure we fetch a cloned
473 2 50       20 # value and set it.
474 2         9 $attr->set_value( $self, $self->$reader_method );
475 2         643 }
476 4         12 );
477              
478             if ( $is eq 'ro' ) {
479             _debug("Adding read-only reader for $name");
480             $meta->add_method( $reader => $reader_method );
481             }
482             elsif ( $reader ne $writer ) {
483             _debug("Adding separate readers and writers for $name");
484             $meta->add_method( $reader => $reader_method );
485             $meta->add_method( $writer => $writer_method );
486             }
487 8         12 else {
488 8     11   10 _debug("Adding overloaded reader/writer for $name");
  8         2029  
489 8         25 $meta->add_method(
490             $reader => sub ( $self, @value ) {
491             _debug( "Args for overloaded reader/writer for $name", [ $self, @value ] );
492             return @value == 0
493 8         71 ? $self->$reader_method
494             : $self->$writer_method(@value);
495 4         23 }
496             );
497 4 100       616 }
    100          
498 2         9 return %opt_for;
499 2         6 }
500              
501             return $MooseX::Extended::Debug // $ENV{MOOSEX_EXTENDED_DEBUG}; # suppress "once" warnings
502 1         5 }
503 1         4  
504 1         37 return unless _should_debug();
505             if (@data) { # yup, still want multidispatch
506             require Data::Printer;
507 1         5 my $data = Data::Printer::np(@data);
508 6         9 $message = "$message: $data";
509 6     6   8 }
  6         809  
  6         10  
510 6         22 say STDERR $message;
511 6 100       21 }
512              
513             1;
514              
515 1         6  
516             =pod
517 4         168  
518             =encoding UTF-8
519              
520 226     232   294 =head1 NAME
  226         251  
521 226   33     2931  
522             MooseX::Extended::Core - Internal module for MooseX::Extended
523              
524 157     157   213 =head1 VERSION
  157         205  
  157         263  
  157         181  
525 157 50       340  
526 0 0         version 0.33
527 0            
528 0           =head1 DESCRIPTION
529 0            
530             This is not for public consumption. Provides the C<field> and C<param>
531 0           functions to L<MooseX::Extended> and L<MooseX::Extended::Role>.
532              
533             =head1 AUTHOR
534              
535             Curtis "Ovid" Poe <curtis.poe@gmail.com>
536              
537             =head1 COPYRIGHT AND LICENSE
538              
539             This software is Copyright (c) 2022 by Curtis "Ovid" Poe.
540              
541             This is free software, licensed under:
542              
543             The Artistic License 2.0 (GPL Compatible)
544              
545             =cut