File Coverage

blib/lib/MooseX/ErsatzMethod.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 7 7 100.0
pod n/a
total 23 25 92.0


line stmt bran cond sub pod time code
1 1     1   27070 use 5.008;
  1         4  
  1         50  
2 1     1   7 use strict;
  1         2  
  1         41  
3 1     1   7 use warnings;
  1         7  
  1         102  
4              
5             package MooseX::ErsatzMethod;
6              
7             BEGIN {
8 1     1   3 $MooseX::ErsatzMethod::AUTHORITY = 'cpan:TOBYINK';
9 1         74 $MooseX::ErsatzMethod::VERSION = '0.004';
10             }
11              
12             my %METAROLES;
13             BEGIN {
14 1     1   25 %METAROLES = (
15             role => [ 'MooseX::ErsatzMethod::Trait::Role' ],
16             application_to_class => [ 'MooseX::ErsatzMethod::Trait::ApplicationToClass' ],
17             application_to_role => [ 'MooseX::ErsatzMethod::Trait::ApplicationToRole' ],
18             application_to_instance => [ 'MooseX::ErsatzMethod::Trait::ApplicationToInstance' ],
19             )
20             };
21              
22 1     1   1122 use Module::Runtime ();
  1         1912  
  1         25  
23 1     1   496 use Moose ();
  0            
  0            
24             use Moose::Exporter;
25              
26             BEGIN {
27             package MooseX::ErsatzMethod::Meta::Method;
28             our $AUTHORITY = 'cpan:TOBYINK';
29             our $VERSION = '0.004';
30             use Moose;
31             has code => (
32             is => 'ro',
33             isa => 'CodeRef',
34             required => 1,
35             );
36             has name => (
37             is => 'ro',
38             isa => 'Str',
39             required => 1,
40             );
41             has associated_role => (
42             is => 'ro',
43             isa => 'Object',
44             required => 0,
45             );
46             sub apply_to_class
47             {
48             my ($self, $class) = @_;
49             return if $class->find_method_by_name($self->name);
50             $class->add_method($self->name, $self->code);
51             }
52             $INC{ Module::Runtime::module_notional_filename(__PACKAGE__) } ||= __FILE__;
53             }
54              
55             BEGIN {
56             package MooseX::ErsatzMethod::Trait::Role;
57             our $AUTHORITY = 'cpan:TOBYINK';
58             our $VERSION = '0.004';
59             use Moose::Role;
60             has ersatz_methods => (
61             traits => ['Hash'],
62             is => 'ro',
63             isa => 'HashRef[MooseX::ErsatzMethod::Meta::Method]',
64             lazy_build => 1,
65             handles => {
66             all_ersatz_methods => 'values',
67             _add_ersatz_method => 'set',
68             },
69             );
70             sub _build_ersatz_methods { +{} };
71             sub add_ersatz_method
72             {
73             my ($meta, $method) = @_;
74             $meta->_add_ersatz_method($method->name => $method);
75             }
76             sub apply_all_ersatz_methods_to_class
77             {
78             my ($self, $class) = @_;
79             for ($self->all_ersatz_methods)
80             {
81             next if $self->has_method($_->name);
82             $_->apply_to_class($class);
83             }
84             }
85             sub composition_class_roles
86             {
87             return 'MooseX::ErsatzMethod::Trait::Composite';
88             }
89             $INC{ Module::Runtime::module_notional_filename(__PACKAGE__) } ||= __FILE__;
90             };
91              
92             BEGIN {
93             package MooseX::ErsatzMethod::Trait::Composite;
94             our $AUTHORITY = 'cpan:TOBYINK';
95             our $VERSION = '0.004';
96             use Moose::Role;
97             with qw(MooseX::ErsatzMethod::Trait::Role);
98             around apply_params => sub
99             {
100             my $orig = shift;
101             my $self = shift;
102             $self->$orig(@_);
103            
104             $self = Moose::Util::MetaRole::apply_metaroles(
105             for => $self,
106             role_metaroles => \%METAROLES,
107             );
108             $self->_merge_ersatz_methods;
109             return $self;
110             };
111             sub _merge_ersatz_methods
112             {
113             my $self = shift;
114             foreach my $role (@{ $self->get_roles })
115             {
116             next unless Moose::Util::does_role(
117             $role,
118             'MooseX::ErsatzMethod::Trait::Role',
119             );
120             $self->add_ersatz_method($_) for $role->all_ersatz_methods;
121             }
122             }
123             $INC{ Module::Runtime::module_notional_filename(__PACKAGE__) } ||= __FILE__;
124             };
125              
126             BEGIN {
127             package MooseX::ErsatzMethod::Trait::ApplicationToClass;
128             our $AUTHORITY = 'cpan:TOBYINK';
129             our $VERSION = '0.004';
130             use Moose::Role;
131             before apply => sub
132             {
133             my ($meta, $role, $class) = @_;
134             return unless Moose::Util::does_role(
135             $role,
136             'MooseX::ErsatzMethod::Trait::Role',
137             );
138             $role->apply_all_ersatz_methods_to_class($class);
139             };
140             $INC{ Module::Runtime::module_notional_filename(__PACKAGE__) } ||= __FILE__;
141             };
142              
143             BEGIN {
144             package MooseX::ErsatzMethod::Trait::ApplicationToRole;
145             our $AUTHORITY = 'cpan:TOBYINK';
146             our $VERSION = '0.004';
147             use Moose::Role;
148             before apply => sub
149             {
150             my ($meta, $role1, $role2) = @_;
151             $role2 = Moose::Util::MetaRole::apply_metaroles(
152             for => $role2,
153             role_metaroles => \%METAROLES,
154             );
155             $role2->add_ersatz_method($_) for $role1->all_ersatz_methods;
156             };
157             $INC{ Module::Runtime::module_notional_filename(__PACKAGE__) } ||= __FILE__;
158             };
159              
160             BEGIN {
161             package MooseX::ErsatzMethod::Trait::ApplicationToInstance;
162             our $AUTHORITY = 'cpan:TOBYINK';
163             our $VERSION = '0.004';
164             use Moose::Role;
165             $INC{ Module::Runtime::module_notional_filename(__PACKAGE__) } ||= __FILE__;
166             };
167              
168             Moose::Exporter->setup_import_methods(
169             with_meta => [ 'ersatz' ],
170             role_metaroles => \%METAROLES,
171             );
172              
173             sub ersatz
174             {
175             my ($meta, $name, $coderef) = @_;
176            
177             Carp::confess('Ersatz methods can only be created for Moose roles; not classes. Stopped')
178             unless $meta->isa('Moose::Meta::Role');
179            
180             my $method;
181             if (Scalar::Util::blessed($name))
182             {
183             $method = $name;
184             }
185             else
186             {
187             $method = 'MooseX::ErsatzMethod::Meta::Method'->new(
188             code => $coderef,
189             name => $name,
190             associated_role => $meta,
191             );
192             }
193            
194             $meta->add_ersatz_method($method);
195             }
196              
197             1;
198              
199             __END__
200              
201             =head1 NAME
202              
203             MooseX::ErsatzMethod - provide a method implementation that isn't as good as the real thing
204              
205             =head1 SYNOPSIS
206              
207             package Greetable;
208             use Moose::Role;
209             use MooseX::ErsatzMethod;
210            
211             sub greet {
212             my $self = shift;
213             say "Hello ", $self->name;
214             }
215            
216             ersatz name => sub {
217             my $self = shift;
218             return Scalar::Util::refaddr($self);
219             };
220              
221             package Person;
222             use Moose;
223             with 'Greetable';
224             has name => (is => 'ro', isa => 'Str');
225            
226             package Termite;
227             use Moose;
228             with 'Greetable';
229             # no need to implement 'name'.
230              
231             =head1 DESCRIPTION
232              
233             MooseX::ErsatzMethod provides a mechanism for Moose roles to provide fallback
234             implementations of methods that they really want for consuming classes to
235             implement. In the SYNOPSIS section, the C<Greetable> role really wants
236             consuming classes to implement a C<name> method. The C<Termite> class doesn't
237             implement C<name>, but it's OK, because C<Greetable> provides a fallback
238             (albeit rubbish) implementation of the method.
239              
240             B<< But wait! >> I hear you say. Don't roles already work that way? Can't a
241             role provide an implementation of a method which consuming classes can
242             override? Yes, they can. However, the precedence is:
243              
244             consuming class's implementation (wins)
245             role's implementation
246             inherited implementation (e.g. from parent class)
247              
248             That is, the role's method implementation overrides methods inherited from the
249             parent class. An ersatz method implementation sits right at the bottom of the
250             heirarchy; it is only used if the consuming class and its ancestors cannot
251             provide the method. (It still beats C<AUTOLOAD> though.)
252              
253             One other feature of ersatz methods is that they can never introduce role
254             composition conflicts. If you compose two different roles which both provide
255             ersatz method implementations, an arbitrary method implementation is selected.
256              
257             =head2 Functions
258              
259             =over
260              
261             =item C<< ersatz $name => $coderef >>
262              
263             Defines an ersatz function.
264              
265             =back
266              
267             =head2 Metarole Trait
268              
269             Your metarole (i.e. C<< $metarole = Greetable->meta >>) will have the
270             following additional methods:
271              
272             =over
273              
274             =item C<< ersatz_methods >>
275              
276             Returns a name => object hashref of ersatz methods for this class. The
277             objects are instances of L<< MooseX::ErsatzMethod::Meta::Method >>.
278              
279             =item C<< all_ersatz_methods >>
280              
281             Returns just the values (objects) from the C<ersatz_methods> hash.
282              
283             =item C<< add_ersatz_method($name, $coderef) >>
284              
285             Given a name and coderef, creates a L<< MooseX::ErsatzMethod::Meta::Method >>
286             object and adds it to the C<ersatz_methods> hash.
287              
288             =item C<< apply_all_ersatz_methods_to_class($class) >>
289              
290             Given a Moose::Meta::Class object, iterates through C<all_ersatz_methods>
291             applying each to the class. This procedure skips any ersatz method for which
292             this role can provide a real method.
293              
294             =back
295              
296             =head2 MooseX::ErsatzMethod::Meta::Method
297              
298             Instances of this class represent an ersatz method.
299              
300             =over
301              
302             =item C<< new(%attrs) >>
303              
304             Standard Moose constructor.
305              
306             =item C<< code >>
307              
308             The coderef for the method.
309              
310             =item C<< name >>
311              
312             The sub name for the method (not including the package).
313              
314             =item C<< associated_role >>
315              
316             The metarole associated with this method (if any).
317              
318             =item C<< apply_to_class($class) >>
319              
320             Given a Moose::Meta::Class object, installs this method into the class
321             unless the class (or a superclass) already has a method of that name.
322              
323             =back
324              
325             =head1 CAVEATS
326              
327             If you use one-at-a-time role composition, then ersatz methods in one
328             role might end up "beating" a proper method provided by another role.
329              
330             with 'Role1'; with 'Role2'; # No!
331             with qw( Role1 Role2 ); # Yes
332              
333             =head1 BUGS
334              
335             Please report any bugs to
336             L<http://rt.cpan.org/Dist/Display.html?Queue=MooseX-ErsatzMethod>.
337              
338             =head1 SEE ALSO
339              
340             L<Moose::Role>.
341              
342             L<https://speakerdeck.com/u/sartak/p/moose-role-usage-patterns?slide=32>.
343              
344             =head1 AUTHOR
345              
346             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
347              
348             =head1 COPYRIGHT AND LICENCE
349              
350             This software is copyright (c) 2012, 2014 by Toby Inkster.
351              
352             This is free software; you can redistribute it and/or modify it under
353             the same terms as the Perl 5 programming language system itself.
354              
355             =head1 DISCLAIMER OF WARRANTIES
356              
357             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
358             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
359             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
360