File Coverage

blib/lib/MooX/Enumeration.pm
Criterion Covered Total %
statement 151 156 96.7
branch 47 62 75.8
condition 22 42 52.3
subroutine 24 24 100.0
pod 0 5 0.0
total 244 289 84.4


line stmt bran cond sub pod time code
1 11     13   826526 use 5.008001;
  11         77  
2 11     11   60 use strict;
  11         22  
  11         279  
3 11     11   55 use warnings;
  11         29  
  11         353  
4 11     11   60 no warnings 'once';
  11         21  
  11         678  
5              
6             package MooX::Enumeration;
7              
8 11     11   71 use Carp qw(croak);
  11         21  
  11         532  
9 11     11   66 use Scalar::Util qw(blessed);
  11         25  
  11         553  
10 11     11   84 use Sub::Util qw(set_subname);
  11         21  
  11         674  
11 11     11   70 use B qw(perlstring);
  11         21  
  11         11568  
12              
13             our $AUTHORITY = 'cpan:TOBYINK';
14             our $VERSION = '0.010';
15              
16             sub import {
17 12     12   649 my $class = shift;
18 12         30 my $caller = caller;
19 12         40 $class->setup_for($caller);
20             }
21              
22             sub setup_for {
23 12     12 0 22 my $class = shift;
24 12         27 my ($target) = @_;
25            
26 12         21 my ($orig, $installer);
27 12 100 66     137 if ($INC{'Moo/Role.pm'} && Moo::Role->is_role($target)) {
    50 33        
28 1         53 $installer = 'Moo::Role::_install_tracked';
29 1         4 $orig = $Moo::Role::INFO{$target}{exports}{has};
30             }
31             elsif ($Moo::MAKERS{$target} && $Moo::MAKERS{$target}{is_class}) {
32 11         22 $installer = 'Moo::_install_tracked';
33 11   33     41 $orig = $Moo::MAKERS{$target}{exports}{has} || $Moo::MAKERS{$target}{non_methods}{has};
34             }
35             else {
36 0         0 croak("$target does not seem to be a Moo class or role");
37             }
38 12   33     34 $orig ||= $target->can('has');
39 12 50       41 ref($orig) or croak("$target doesn't have a `has` function");
40            
41             $target->$installer(has => sub {
42 13 50   13   6921 if (@_ % 2 == 0) {
        13      
        1      
43 0         0 croak "Invalid options for attribute(s): even number of arguments expected, got " . scalar @_;
44             }
45            
46 13         79 my ($attrs, %spec) = @_;
47 13 50       78 $attrs = [$attrs] unless ref $attrs;
48 13         53 for my $attr (@$attrs) {
49 13         114 %spec = $class->process_spec($target, $attr, %spec);
50 13 100       68 if (delete $spec{moox_enumeration_process_handles}) {
51 12         77 $class->install_delegates($target, $attr, \%spec);
52             }
53 12         90 $orig->($attr, %spec);
54             }
55 12         80198 return;
56 12         144 });
57             }
58              
59             sub process_spec {
60 13     13 0 32 my $class = shift;
61 13         53 my ($target, $attr, %spec) = @_;
62            
63 13         27 my @values;
64            
65             # Handle the type constraint stuff
66 13 50 66     175 if (exists $spec{isa} and exists $spec{enum}) {
    100 66        
    100          
67 0         0 croak "Cannot supply both the 'isa' and 'enum' options";
68             }
69             elsif (blessed $spec{isa} and $spec{isa}->isa('Type::Tiny::Enum')) {
70 1         13 @values = @{ $spec{isa}->values };
  1         4  
71             }
72             elsif (exists $spec{enum}) {
73 11 50       55 croak "Expected arrayref for enum" unless ref $spec{enum} eq 'ARRAY';
74 11         24 @values = @{ delete $spec{enum} };
  11         76  
75 11         4704 require Type::Tiny::Enum;
76 11         168121 $spec{isa} = Type::Tiny::Enum->new(values => \@values);
77             }
78             else {
79             # nothing to do
80 1         7 return %spec;
81             }
82            
83             # Canonicalize handles
84 12 50       1631 if (my $handles = $spec{handles}) {
85            
86 12         43 $spec{moox_enumeration_process_handles} = !!1;
87            
88 12 100 100     128 if (!ref $handles and $handles eq 1) {
    100 66        
89 5         48 $handles = +{ map +( "is_$_" => [ "is", $_ ] ), @values };
90             }
91             elsif (!ref $handles and $handles eq 2) {
92 1         11 $handles = +{ map +( "$attr\_is_$_" => [ "is", $_ ] ), @values };
93             }
94              
95 12 100       53 if (ref $handles eq 'ARRAY') {
96 1 50       20 $handles = +{ map ref($_)?@$_:($_=>[split/_/,$_,2]), @$handles };
97             }
98            
99 12 50       48 if (ref $handles eq 'HASH') {
100 12         56 for my $k (keys %$handles) {
101 37 100       112 next if ref $handles->{$k};
102 5         21 $handles->{$k}=[split/_/,$handles->{$k},2];
103             }
104             }
105            
106 12         40 $spec{handles} = $handles;
107             }
108            
109             # Install moosify stuff
110 12 50       51 if (ref $spec{moosify} eq 'CODE') {
111 0         0 $spec{moosify} = [$spec{moosify}];
112             }
113 12   50     118 push @{ $spec{moosify} ||= [] }, sub {
114 1     1   491016 my $spec = shift;
115 1         9 require MooseX::Enumeration;
116 1         649 require MooseX::Enumeration::Meta::Attribute::Native::Trait::Enumeration;
117 1   50     29731 push @{ $spec->{traits} ||= [] }, 'Enumeration';
  1         11  
118 1 50 33     19 $spec->{handles} ||= $spec->{_orig_handles} if $spec->{_orig_handles};
119 12         28 };
120            
121 12         125 return %spec;
122             }
123              
124             sub install_delegates {
125 12     12 0 95 require Eval::TypeTiny;
126            
127 12         28 my $class = shift;
128 12         42 my ($target, $attr, $spec) = @_;
129            
130 12         23 my %delegates = %{ $spec->{_orig_handles} = delete $spec->{handles} };
  12         66  
131            
132 12         49 for my $method (keys %delegates) {
133 37         86 my ($delegate_type, @delegate_params) = @{ $delegates{$method} };
  37         132  
134 37         105 my $builder = "build_${delegate_type}_delegate";
135            
136 11     11   86 no strict 'refs';
  11         21  
  11         12099  
137 37         222 *{"${target}::${method}"} =
  36         15476  
138             set_subname "${target}::${method}",
139             $class->$builder($target, $method, $attr, $spec, @delegate_params);
140             }
141             }
142              
143             sub _accessor_maker_for {
144 28     28   55 my $class = shift;
145 28         57 my ($target) = @_;
146            
147 28 100 66     247 if ($INC{'Moo/Role.pm'} && Moo::Role->is_role($target)) {
    50 33        
148 2         62 my $dummy = 'MooX::Enumeration::____DummyClass____';
149 2     1   150 eval('package ' # hide from CPAN indexer
  1         7  
  1         2  
  1         6  
  1         7  
  1         2  
  1         6  
150             . "$dummy; use Moo");
151 2         605 return Moo->_accessor_maker_for($dummy);
152             }
153             elsif ($Moo::MAKERS{$target} && $Moo::MAKERS{$target}{is_class}) {
154 26         149 return Moo->_accessor_maker_for($target);
155             }
156             else {
157 0         0 croak "Cannot get accessor maker for $target";
158             }
159             }
160              
161             sub build_is_delegate {
162 28     28 0 64 my $class = shift;
163 28         74 my ($target, $method, $attr, $spec, $match) = @_;
164            
165 28         83 my $MAKER = $class->_accessor_maker_for($target);
166             my ($GET, $CAPTURES) = $MAKER->is_simple_get($attr, $spec)
167             ? $MAKER->generate_simple_get('$_[0]', $attr, $spec)
168 28 100       117812 : ($MAKER->_generate_get($attr, $spec), delete($MAKER->{captures}));
169            
170 28         2970 my $desc = "delegated method $target\::$method";
171            
172 28 100       198 if (ref $match) {
    100          
173 1         527 require match::simple;
174 1         5666 $CAPTURES->{'$match'} = \$match;
175 1         8 return Eval::TypeTiny::eval_closure(
176             description => $desc,
177             source => sprintf(
178             'sub { %s; my $value = %s; match::simple::match($value, $match) }',
179             $class->_build_throw_args($method, 0),
180             $GET,
181             ),
182             environment => $CAPTURES,
183             );
184             }
185             elsif ($spec->{isa}->check($match)) {
186 26         4153 return Eval::TypeTiny::eval_closure(
187             description => $desc,
188             source => sprintf(
189             'sub { %s; (%s) eq %s }',
190             $class->_build_throw_args($method, 0),
191             $GET,
192             perlstring($match),
193             ),
194             environment => $CAPTURES,
195             );
196             }
197             else {
198 1         307 croak sprintf "Attribute $attr cannot be %s", perlstring($match);
199             }
200             }
201              
202             sub build_assign_delegate {
203 9     9 0 19 my $class = shift;
204 9         24 my ($target, $method, $attr, $spec, $newvalue, $match) = @_;
205              
206             croak sprintf "Attribute $attr cannot be %s", perlstring($newvalue)
207 9 50 33     44 unless $spec->{isa}->check($newvalue) || !$spec->{isa};
208              
209 9         1111 my $MAKER = Moo->_accessor_maker_for($target);
210             my ($GET, $CAPTURES) = $MAKER->is_simple_get($attr, $spec)
211             ? $MAKER->generate_simple_get('$_[0]', $attr, $spec)
212 9 100 50     28799 : ($MAKER->_generate_get($attr, $spec), delete($MAKER->{captures})||{});
213            
214             # We can actually use the simple version set even if there's a type constraint,
215             # because we've already checked that $newvalue passes the type constraint!
216             #
217 9         46 my $SET = $MAKER->is_simple_set($attr, do { my %temp = %$spec; delete $temp{coerce}; delete $temp{isa}; \%temp })
  9         19  
  9         17  
  9         33  
218             ? sub {
219 8     8   20 my ($var) = @_;
220 8         25 $MAKER->_generate_simple_set('$_[0]', $attr, $spec, $var);
221             }
222             : sub { # that allows us to avoid going down this yucky code path
223 1     1   3 my ($var) = @_;
224 1         3 my $code = $MAKER->_generate_set($attr, $spec);
225 1 50       323 $CAPTURES = { %$CAPTURES, %{ delete($MAKER->{captures}) or {} } }; # merge environments
  1         7  
226 1         19 $code = sprintf "do { local \@_ = (\$_[0], $var); %s }", $code;
227 1         5 $code;
228 9 100       1101 };
229              
230 9         110 my $err = 'Method %s cannot be called when attribute %s has value %s';
231 9         23 my $desc = "delegated method $target\::$method";
232              
233 9 100       30 if (ref $match) {
    100          
234 3         989 require match::simple;
235 3         11348 my $_SET = $SET->(perlstring $newvalue);
236 3         86 $CAPTURES->{'$match'} = \$match;
237 3         13 return Eval::TypeTiny::eval_closure(
238             description => $desc,
239             source => sprintf(
240             'sub { %s; my $value = %s; return $_[0] if $value eq %s; match::simple::match($value, $match) ? (%s) : Carp::croak(sprintf %s, %s, %s, $value); $_[0] }',
241             $class->_build_throw_args($method, 0),
242             $GET,
243             perlstring($newvalue),
244             $_SET,
245             perlstring($err),
246             perlstring($method),
247             perlstring($attr),
248             ),
249             environment => $CAPTURES,
250             );
251             }
252             elsif (defined $match) {
253 3 50       14 $spec->{isa}->check($match)
254             or croak sprintf "Attribute $attr cannot be %s", perlstring($match);
255 3         46 my $_SET = $SET->(perlstring $newvalue);
256 3         61 return Eval::TypeTiny::eval_closure(
257             description => $desc,
258             source => sprintf(
259             'sub { %s; my $value = %s; return $_[0] if $value eq %s; ($value eq %s) ? (%s) : Carp::croak(sprintf %s, %s, %s, $value); $_[0] }',
260             $class->_build_throw_args($method, 0),
261             $GET,
262             perlstring($newvalue),
263             perlstring($match),
264             $_SET,
265             perlstring($err),
266             perlstring($method),
267             perlstring($attr),
268             ),
269             environment => $CAPTURES,
270             );
271             }
272             else {
273 3         10 my $_SET = $SET->(perlstring $newvalue);
274 3         84 return Eval::TypeTiny::eval_closure(
275             description => $desc,
276             source => sprintf(
277             'sub { %s; %s; $_[0] }',
278             $class->_build_throw_args($method, 0),
279             $_SET,
280             ),
281             environment => $CAPTURES,
282             );
283             }
284             }
285              
286             sub _build_throw_args {
287 36     36   77 my $class = shift;
288 36         86 my ($method, $n) = @_;
289 36         487 sprintf(
290             'Carp::croak(sprintf "Method %%s expects %%d arguments", %s, %d) if @_ != %d;',
291             perlstring($method),
292             $n,
293             $n+1,
294             );
295             }
296              
297             1;
298              
299             __END__
300              
301             =pod
302              
303             =encoding utf-8
304              
305             =head1 NAME
306              
307             MooX::Enumeration - shortcuts for working with enum attributes in Moo
308              
309             =head1 SYNOPSIS
310              
311             Given this class:
312              
313             package MyApp::Result {
314             use Moo;
315             use Types::Standard qw(Enum);
316             has status => (
317             is => "rw",
318             isa => Enum[qw/ pass fail /],
319             );
320             }
321              
322             It's quite common to do this kind of thing:
323              
324             if ( $result->status eq "pass" ) { ... }
325              
326             But if you're throwing strings around, it can be quite easy to mistype
327             them:
328              
329             if ( $result->status eq "apss" ) { ... }
330              
331             And the comparison silently fails. Instead, let's define the class like
332             this:
333              
334             package MyApp::Result {
335             use Moo;
336             use MooX::Enumeration;
337             use Types::Standard qw(Enum);
338             has status => (
339             is => "rw",
340             isa => Enum[qw/ pass fail /],
341             handles => [qw/ is_pass is_fail /],
342             );
343             }
344              
345             So you can use the class like this:
346              
347             if ( $result->is_pass ) { ... }
348              
349             Yay!
350              
351             =head1 DESCRIPTION
352              
353             This is a Moo implementation of L<MooseX::Enumeration>. All the features
354             from the Moose version should work here.
355              
356             Passing C<< traits => ["Enumeration"] >> to C<has> is not needed with
357             MooX::Enumeration. This module's magic is automatically applied to all
358             attributes with a L<Type::Tiny::Enum> type constraint.
359              
360             Simple example:
361              
362             package MyClass {
363             use Moo;
364             use MooX::Enumeration;
365            
366             has xyz => (is => "ro", enum => [qw/foo bar baz/], handles => 1);
367             }
368              
369             C<< MyClass->new(xyz => "quux") >> will throw an error.
370              
371             Objects of the class will have C<< $object->is_foo >>, C<< $object->is_bar >>,
372             and C<< $object->is_baz >> methods.
373              
374             If you use C<< handles => 2 >>, then you get C<< $object->xyz_is_foo >>, etc
375             methods.
376              
377             For more details of method delegation, see L<MooseX::Enumeration>.
378              
379             =head2 Use in roles
380              
381             Since version 0.009, this will work in roles too, but with a caveat.
382              
383             The coderef to be installed into the class is built when defining the role,
384             and not when composing the role with the class, so the coderef has no
385             knowledge of the class. In particular, it doesn't know anything about what
386             kind of reference the blessed object will be (hashref, arrayref, etc), so just
387             assumes that it will be a hashref, and that the hash key used for the
388             attribute will match the attribute name. Unless you're using non-hashref
389             objects or you're doing unusual things with Moo internals, these assumptions
390             will usually be safe.
391              
392             =head1 BUGS
393              
394             Please report any bugs to
395             L<http://rt.cpan.org/Dist/Display.html?Queue=MooX-Enumeration>.
396              
397             =head1 SEE ALSO
398              
399             L<MooseX::Enumeration>.
400              
401             L<Type::Tiny::Enum>.
402              
403             L<Moo>.
404              
405             =head1 AUTHOR
406              
407             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
408              
409             =head1 COPYRIGHT AND LICENCE
410              
411             This software is copyright (c) 2018 by Toby Inkster.
412              
413             This is free software; you can redistribute it and/or modify it under
414             the same terms as the Perl 5 programming language system itself.
415              
416             =head1 DISCLAIMER OF WARRANTIES
417              
418             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
419             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
420             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
421