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   165932 use warnings;
  19         70  
6 19     19   87 use parent 'Exporter';
  19         42  
  19         466  
7 19     19   101 use Moose::Util qw(
  19         41  
  19         130  
8 19         140 add_method_modifier
9             throw_exception
10             );
11 19     19   1951 use MooseX::Extended::Types qw(
  19         233928  
12 19         209 ArrayRef
13             Bool
14             Dict
15             Enum
16             NonEmptyStr
17             Optional
18             Str
19             Undef
20             compile_named
21             );
22 19     19   14105 use Module::Load 'load';
  19         68  
23 19     19   102522 use feature qw(signatures postderef);
  19         169  
  19         201  
24 19     19   1324 no warnings qw(experimental::signatures experimental::postderef);
  19         38  
  19         2252  
25 19     19   108  
  19         35  
  19         784  
26             use Storable 'dclone';
27 19     19   11077 use Ref::Util qw(
  19         52237  
  19         1369  
28 19         942 is_plain_arrayref
29             is_coderef
30             );
31 19     19   1424 use Carp 'croak';
  19         1518  
32 19     19   109 #
  19         38  
  19         1414  
33              
34             our $VERSION = '0.34';
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   121  
  19         31  
  19         42400  
50              
51 96     96   9189 warnings::register_categories(
52 96     96   5998 '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   226 @_ = ( $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         249 # arrays to hashes (if ( $args{excludes}{$feature} ) ...) and returns the
76 69         297 # 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   217 }
84             if ( my $includes = $args->{includes} ) {
85 71         218 if ( !ref $includes ) {
86 142 100 100     689 $args->{includes} = { $includes => undef };
87 4         15 }
88             elsif ( is_plain_arrayref($includes) ) {
89             $args->{includes} = { map { $_ => undef } $includes->@* };
90 71 100       262 }
91 4 100       21 else {
    100          
92 1         4 # let anything else just fail in type checking
93             }
94             }
95 2         5  
  2         7  
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     462 role => compile_named( _default_import_list(), _role_excludes() )
103 71         318 };
104 71   66     820 eval {
105             $check->{ $args->{_import_type} }->( $args->%* );
106 71         180 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         593 #
112 69         4256 # BEGIN failed--compilation aborted at ...
113 71 100       343047 #
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         658 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         942 );
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         199 }
141 69         305  
  11         44  
142             my $for_class = $params{for_class};
143             my $config = $CONFIG_FOR{$for_class};
144 69         221  
145 69         261 if ( $config->{debug} ) {
146             $MooseX::Extended::Debug = $config->{debug};
147             }
148 69     69   175  
  69         137  
  69         129  
  69         229  
  69         146  
149 69         185 if ( _should_debug() ) {
150 69         156 foreach my $feature (qw/includes excludes/) {
151             if ( exists $config->{$feature} ) {
152 69 50       338 foreach my $category ( sort keys $config->{$feature}->%* ) {
153 0         0 _debug("$for_class $feature '$category'");
154             }
155             }
156 69 50       244 }
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         403 also => ['Moose'],
167 69         7772 );
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   89704 field
  18         41  
185             param
186 18         174 /
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   156783 true
  18         37  
204             field
205 18         133 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   1118642 if ($requested) {
  36         67  
225             return $requested->@*;
226             }
227             elsif ($defaults) {
228             return $defaults->@*;
229             }
230             return;
231             }
232 36         243  
  144         698925  
233             my $includes = $config->{includes} or return;
234              
235             state $requirements_for = {
236 5     5   9 multi => {
  5         13  
  5         7  
  5         7  
237 5 100       20 version => v5.26.0,
    100          
238 1         9 import => undef,
239             module => 'Syntax::Keyword::MultiSub',
240             },
241 1         11 async => {
242             version => v5.26.0,
243 3         20 import => undef,
244             module => 'Future::AsyncAwait',
245             },
246 69     69   164 method => {
  69         129  
  69         114  
  69         99  
247 69 100       512 version => v5.0.0,
248             import => ['method'],
249 3         4 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, 'try' );
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   4 }
  3         6  
270 3         4  
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         9 load $required->{module};
276             $required->{module}->import::into( $for_class, _with_imports( $includes->{$feature}, $required->{import} ) );
277             }
278 4         59 }
279 4         16  
280 5 50       259 $opt_for{is} //= 'ro';
281 5 50 33     177 $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       31 my %options = %opt_for; # copy each time to avoid overwriting
287 3 50       11 $options{init_arg} //= $attr;
288              
289 5         21 # in case they're inheriting an attribute
290 5         4263 $options{init_arg} =~ s/\A\+//;
291             _add_attribute( 'param', $meta, $attr, %options );
292             }
293             }
294 32     32 0 151913  
  32         71  
  32         76  
  32         99  
  32         52  
295 32   100     239 $opt_for{is} //= 'ro';
296 32   100     170 $opt_for{_call_level} //= 1;
297 32   50     164  
298             # "has [@attributes]" versus "has $attribute"
299             foreach my $attr ( is_plain_arrayref($name) ? @$name : $name ) {
300 32 100       137 my %options = %opt_for; # copy each time to avoid overwriting
301 36         17666 if ( defined( my $init_arg = $options{init_arg} ) ) {
302 36   66     196 $init_arg =~ /\A_/ or throw_exception(
303             'InvalidAttributeDefinition',
304             attribute_name => $name,
305 36         118 class_name => $meta->name,
306 36         144 messsage => "A defined 'field.init_arg' must begin with an underscore: '$init_arg'",
307             );
308             }
309              
310 16     16 0 43224 $options{init_arg} //= undef;
  16         36  
  16         32  
  16         52  
  16         28  
311 16   100     108 if ( $options{builder} || $options{default} ) {
312 16   50     95 $options{lazy} //= 1;
313             }
314              
315 16 50       69 _add_attribute( 'field', $meta, $attr, %options );
316 16         72 }
317 16 100       69 }
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     73 my $orig_name = $name;
327 15 100 100     98 $name =~ s/\A\+//;
328 12   100     58 unless ( _is_valid_method_name($name) ) {
329             throw_exception(
330             'InvalidAttributeDefinition',
331 15         154 attribute_name => $orig_name,
332             class_name => $meta->name,
333             messsage => "Illegal attribute name, '$name'",
334             );
335 51     51   84 }
  51         88  
  51         76  
  51         85  
  51         169  
  51         79  
336 51         239  
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         105 };
344 51         116  
345 51 100       141 if ( is_coderef( $opt_for{builder} ) ) {
346 1         7 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         8 OPTION: foreach my $option ( keys $shortcut_for->%* ) {
355 4     4   6 next unless exists $opt_for{$option};
  4         14  
  4         10  
356 6     6   9 no warnings 'numeric'; ## no critic (TestingAndDebugging::ProhibitNoWarning)
  6         14  
  6         8  
  6         10  
357 3     3   5 if ( 1 == length( $opt_for{$option} ) && 1 == $opt_for{$option} ) {
  3         9  
  3         4  
  3         6  
358 9     9   13 my $option_name = $shortcut_for->{$option}->($name);
  9         22  
  9         18  
  9         18  
359 0     0   0 $opt_for{$option} = $option_name;
  0         0  
  0         0  
  0         0  
360 50         306 }
361             unless ( _is_valid_method_name( $opt_for{$option} ) ) {
362 50 100       275 throw_exception(
363 2         8 'InvalidAttributeDefinition',
364 2         8 attribute_name => $orig_name,
365 2 50       7 class_name => $meta->name,
366 2         12 messsage => "Attribute '$orig_name' has an invalid option name, $option => '$opt_for{$option}'",
367 2         85 );
368             }
369             }
370              
371 50         207 if ( 'rwp' eq $opt_for{is} ) {
372 248 100       462 $opt_for{writer} = "_set_$name";
373 19     19   182 }
  19         58  
  19         9576  
374 23 100 66     148  
375 20         59 if ( exists $opt_for{writer} && defined $opt_for{writer} ) {
376 20         48 $opt_for{is} = 'rw';
377             }
378 23 100       54  
379 1         7 %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       163  
389 2         6 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     178 if $] ge '5.028'
393 11         27 and warnings::enabled_at_level( 'MooseX::Extended::naked_fields', $call_level );
394             }
395              
396 49         184 delete $opt_for{_call_level};
397             _debug( "Setting $attr_type, '$orig_name'", \%opt_for );
398 49 50 66     474 $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         96 elsif ( _is_valid_method_name($clone) ) {
414 49         201 $use_method = 1;
415 49         329 }
416             elsif ( is_coderef($clone) ) {
417             $use_coderef = 1;
418 78     78   111 }
  78         112  
  78         98  
419 78 100       179 else {
420 77         772 throw_exception(
421             'InvalidAttributeDefinition',
422             attribute_name => $name,
423 49     49   78 class_name => $meta->name,
  49         70  
  49         82  
  49         144  
  49         74  
424 49 100       309 messsage => "Attribute '$name' has an invalid option value, clone => '$clone'",
425             );
426 19     19   157 }
  19         36  
  19         15298  
427              
428 4         9 # here be dragons ...
429 4 100 66     17 _debug("Adding cloning for $name");
    100          
    50          
430 2         5 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         2 _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         11 my $attr = $meta->get_attribute($name);
449 4   33     17 $new_value
450 4   66     12 = !ref $new_value ? $new_value
451 4         8 : $use_dclone ? dclone($new_value)
452 4         6 : $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   26 $new_value = ref $new_value ? dclone($new_value) : $new_value;
  24     24   3077  
  24     24   33  
        24      
455 24         67 $attr->set_value( $self, $new_value );
456 24         64 return $new_value;
457 24         183 };
458 24 50       3506  
459             # this fixes a bug where we could set the value in the constructor
460 24 50 66     605 # but it would remain a reference to the original data, so we could do
    100          
461             # this:
462             #
463 4         15 # my $date = DateTime->now;
464             # my $object = Some::Classs->new( created => $date );
465 2     3   3 #
  3         963  
  2         4  
  2         3  
466 2         15 # 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     65 $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       48 # value and set it.
474 2         10 $attr->set_value( $self, $self->$reader_method );
475 2         669 }
476 4         17 );
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         10 else {
488 8     11   8 _debug("Adding overloaded reader/writer for $name");
  8         2039  
489 8         31 $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         60 ? $self->$reader_method
494             : $self->$writer_method(@value);
495 4         24 }
496             );
497 4 100       668 }
    100          
498 2         8 return %opt_for;
499 2         5 }
500              
501             return $MooseX::Extended::Debug // $ENV{MOOSEX_EXTENDED_DEBUG}; # suppress "once" warnings
502 1         5 }
503 1         8  
504 1         41 return unless _should_debug();
505             if (@data) { # yup, still want multidispatch
506             require Data::Printer;
507 1         12 my $data = Data::Printer::np(@data);
508 6         9 $message = "$message: $data";
509 6     6   9 }
  6         787  
  6         8  
510 6         24 say STDERR $message;
511 6 100       42 }
512              
513             1;
514              
515 1         8  
516             =pod
517 4         169  
518             =encoding UTF-8
519              
520 226     232   330 =head1 NAME
  226         288  
521 226   33     3143  
522             MooseX::Extended::Core - Internal module for MooseX::Extended
523              
524 157     157   230 =head1 VERSION
  157         228  
  157         239  
  157         189  
525 157 50       331  
526 0 0         version 0.34
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