File Coverage

blib/lib/MooX/Enumeration.pm
Criterion Covered Total %
statement 150 155 96.7
branch 45 60 75.0
condition 19 39 48.7
subroutine 24 24 100.0
pod 0 5 0.0
total 238 283 84.1


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