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