File Coverage

blib/lib/MooseX/Interface.pm
Criterion Covered Total %
statement 160 171 93.5
branch 21 30 70.0
condition 6 15 40.0
subroutine 44 49 89.8
pod 5 13 38.4
total 236 278 84.8


line stmt bran cond sub pod time code
1 5     5   163987 use 5.010;
  5         20  
  5         204  
2 5     5   30 use strict;
  5         11  
  5         162  
3 5     5   29 use warnings;
  5         14  
  5         139  
4 5     5   5603 use utf8;
  5         50  
  5         29  
5 5     5   7437 use Moose::Exporter 0 ();
  5         940126  
  5         151  
6 5     5   5420 use Moose::Role 2.00 ();
  5         1734629  
  5         161  
7 5     5   51 use Moose::Util 0 ();
  5         72  
  5         115  
8 5     5   30 use Moose::Util::MetaRole 0 ();
  5         92  
  5         148  
9 5     5   27 use constant 1.01 ();
  5         134  
  5         104  
10 5     5   69 use Class::Load 0 ();
  5         67  
  5         455  
11              
12             {
13             package MooseX::Interface;
14            
15             BEGIN {
16 5     5   10 $MooseX::Interface::AUTHORITY = 'cpan:TOBYINK';
17 5         10 $MooseX::Interface::VERSION = '0.007';
18            
19 5         20 *requires = \&Moose::Role::requires;
20 5         2425 *excludes = \&Moose::Role::excludes;
21             }
22            
23             sub test_case (&;$)
24             {
25 5     5 1 482 Class::MOP::class_of( (scalar caller)[0] )->add_test_case(@_);
26             }
27            
28             sub const
29             {
30 0     0 1 0 my ($meta, $name, $value) = @_;
31 0         0 $meta->add_constant($name, $value);
32             }
33            
34             sub extends
35             {
36 2     2 1 205 my ($meta, $other) = @_;
37 2         12 Class::Load::load_class($other);
38 2 50 33     84 confess("Tried to extent $other, but $other is not an interface; died")
39             unless $other->meta->can('is_interface') && $other->meta->is_interface;
40 2         19 Moose::Util::ensure_all_roles($meta->name, $other);
41             }
42            
43             sub one ()
44             {
45 9   33 9 1 3294 my $meta = shift || Class::MOP::class_of( (scalar caller)[0] );
46 9         39 $meta->check_interface_integrity;
47 7         105 return 1;
48             }
49            
50             my ($import, $unimport) = Moose::Exporter->build_import_methods(
51             with_meta => [qw( extends excludes const requires one )],
52             as_is => [qw( test_case )],
53             );
54            
55             sub unimport
56             {
57 0     0   0 goto $unimport;
58             }
59            
60             sub import
61             {
62             # my $caller = caller;
63             # Hook::AfterRuntime::after_runtime {
64             # $caller->meta->check_interface_integrity;
65             # };
66 10     10   20267 goto $import;
67             }
68              
69             sub init_meta
70             {
71 10     10 1 1027 my $class = shift;
72 10         50 my %options = @_;
73            
74 10         22 my $iface = $options{for_class};
75 10         71 Moose::Role->init_meta(%options);
76            
77 10         38473 Moose::Util::MetaRole::apply_metaroles(
78             for => $iface,
79             role_metaroles => {
80             role => ['MooseX::Interface::Trait::Role'],
81             }
82             );
83            
84 10         13726 Class::MOP::class_of($iface)->is_interface(1);
85             }
86             }
87              
88             {
89             package MooseX::Interface::Meta::Method::Constant;
90 5     5   35 use Moose;
  5         9  
  5         32  
91             extends 'Moose::Meta::Method';
92            
93             BEGIN {
94 5     5   39866 $MooseX::Interface::Meta::Method::Constant::AUTHORITY = 'cpan:TOBYINK';
95 5         201 $MooseX::Interface::Meta::Method::Constant::VERSION = '0.007';
96             }
97             }
98              
99             {
100             package MooseX::Interface::Meta::Method::Required;
101 5     5   54 use Moose;
  5         12  
  5         26  
102             extends 'Moose::Meta::Role::Method::Required';
103            
104             BEGIN {
105 5     5   34114 $MooseX::Interface::Meta::Method::Required::AUTHORITY = 'cpan:TOBYINK';
106 5         180 $MooseX::Interface::Meta::Method::Required::VERSION = '0.007';
107             }
108             }
109              
110             {
111             package MooseX::Interface::Meta::Method::Required::WithSignature;
112 5     5   42 use Moose;
  5         8  
  5         92  
113 5     5   34005 use Moose::Util::TypeConstraints ();
  5         13  
  5         279  
114             extends 'MooseX::Interface::Meta::Method::Required';
115            
116             BEGIN {
117 5     5   13 $MooseX::Interface::Meta::Method::Required::WithSignature::AUTHORITY = 'cpan:TOBYINK';
118 5         927 $MooseX::Interface::Meta::Method::Required::WithSignature::VERSION = '0.007';
119             }
120            
121             has signature => (
122             is => 'ro',
123             isa => 'ArrayRef',
124             required => 1,
125             );
126            
127             sub check_signature
128             {
129 8     8 0 16 my ($meta, $args) = @_;
130 8         353 my $sig = $meta->signature;
131            
132 8         14 for my $i (0 .. $#{$sig})
  8         25  
133             {
134 11         424 my $tc = Moose::Util::TypeConstraints::find_type_constraint($sig->[$i]);
135 11 100       1263 return 0 unless $tc->check($args->[$i]);
136             }
137            
138 4         494 return 1;
139             }
140             }
141              
142             {
143             package MooseX::Interface::Meta::TestReport;
144 5     5   28 use Moose;
  5         9  
  5         29  
145 5     5   34479 use namespace::clean;
  5         15  
  5         54  
146            
147             BEGIN {
148 5     5   1655 $MooseX::Interface::Meta::TestReport::AUTHORITY = 'cpan:TOBYINK';
149 5         794 $MooseX::Interface::Meta::TestReport::VERSION = '0.007';
150             }
151            
152             use overload
153 7     7   6187 q[bool] => sub { my $self = shift; !scalar(@{ $self->failed }) },
  7         9  
  7         229  
154 1     1   1050 q[0+] => sub { my $self = shift; scalar(@{ $self->failed }) },
  1         2  
  1         46  
155 1 50   1   351 q[""] => sub { my $self = shift; scalar(@{ $self->failed }) ? 'not ok' : 'ok' },
  1         3  
  1         36  
156 0     0   0 q[@{}] => sub { my $self = shift; $self->failed },
  0         0  
157 5         92 fallback => 1,
158 5     5   32 ;
  5         9  
159            
160             has [qw/ passed failed /] => (
161             is => 'ro',
162             isa => 'ArrayRef',
163             required => 1,
164             );
165             }
166              
167             {
168             package MooseX::Interface::Meta::TestCase;
169 5     5   2946 use Moose;
  5         11  
  5         25  
170 5     5   39208 use namespace::clean;
  5         13  
  5         32  
171            
172             BEGIN {
173 5     5   1393 $MooseX::Interface::Meta::TestCase::AUTHORITY = 'cpan:TOBYINK';
174 5         566 $MooseX::Interface::Meta::TestCase::VERSION = '0.007';
175             }
176            
177             has name => (
178             is => 'ro',
179             isa => 'Str',
180             required => 1,
181             );
182            
183             has code => (
184             is => 'ro',
185             isa => 'CodeRef',
186             required => 1,
187             );
188            
189             has associated_interface => (
190             is => 'ro',
191             isa => 'Object',
192             predicate => 'has_associated_interface',
193             );
194            
195             sub test_instance
196             {
197 36     36 0 43 my ($self, $instance) = @_;
198 36         41 local $_ = $instance;
199 36         1211 $self->code->(@_);
200             }
201             }
202              
203             {
204             package MooseX::Interface::Trait::Role;
205 5     5   2307 use Moose::Role;
  5         8  
  5         32  
206 5     5   30758 use namespace::clean;
  5         13  
  5         33  
207 5     5   1520 use overload ();
  5         12  
  5         254  
208            
209             BEGIN {
210 5     5   11 $MooseX::Interface::Trait::Role::AUTHORITY = 'cpan:TOBYINK';
211 5         7354 $MooseX::Interface::Trait::Role::VERSION = '0.007';
212             }
213            
214             requires qw(
215             name
216             calculate_all_roles
217             get_method_list
218             add_method
219             add_required_methods
220             get_after_method_modifiers_map
221             get_before_method_modifiers_map
222             get_around_method_modifiers_map
223             get_override_method_modifiers_map
224             );
225            
226             has is_interface => (
227             is => 'rw',
228             isa => 'Bool',
229             default => 0,
230             );
231            
232             has test_cases => (
233             is => 'ro',
234             isa => 'ArrayRef[MooseX::Interface::Meta::TestCase]',
235             default => sub { [] },
236             );
237            
238             has integrity_checked => (
239             is => 'rw',
240             isa => 'Bool',
241             default => 0,
242             );
243            
244             has installed_modifiers => (
245             is => 'ro',
246             isa => 'HashRef[Int]',
247             default => sub { +{} },
248             );
249            
250             before apply => sub
251             {
252             my $meta = shift;
253             $meta->check_interface_integrity
254             unless $meta->integrity_checked;
255             };
256            
257             around add_required_methods => sub
258             {
259             my $orig = shift;
260             my $meta = shift;
261             my @required;
262            
263             while (@_)
264             {
265             my $meth = shift;
266             my $sign = ( ref $_[0] or not defined $_[0] ) ? shift : undef;
267             push @required, $sign
268             ? 'MooseX::Interface::Meta::Method::Required::WithSignature'->new(name => $meth, signature => $sign)
269             : 'MooseX::Interface::Meta::Method::Required'->new(name => $meth)
270             }
271            
272             foreach my $r (@required)
273             {
274             next unless $r->can('check_signature');
275            
276             my $modifier = sub {
277             my ($self, @args) = @_;
278             $r->check_signature(\@args) or die sprintf(
279             "method call '%s' on object %s did not conform to signature defined in interface %s",
280             $r->name,
281             overload::StrVal($self),
282             $meta->name,
283             );
284             };
285            
286             $meta->installed_modifiers->{$r->name} = Scalar::Util::refaddr($modifier);
287             $meta->add_before_method_modifier($r->name, $modifier);
288             }
289            
290             return $meta->$orig(@required);
291             };
292            
293             sub add_constant
294             {
295 0     0 0 0 my ($meta, $name, $value) = @_;
296             $meta->add_method(
297             $name => 'MooseX::Interface::Meta::Method::Constant'->wrap(
298 0     0   0 sub () { $value },
299 0         0 name => $name,
300             package_name => $meta->name,
301             ),
302             );
303             }
304            
305             sub add_test_case
306             {
307 5     5 0 34 my ($meta, $coderef, $name) = @_;
308 5 50       13 if (blessed $coderef)
309             {
310 0         0 push @{ $meta->test_cases }, $coderef;
  0         0  
311             }
312             else
313             {
314 5   33     12 $name //= sprintf("%s test case %d", $meta->name, 1 + @{ $meta->test_cases });
  0         0  
315 5         4 push @{ $meta->test_cases }, 'MooseX::Interface::Meta::TestCase'->new(
  5         169  
316             name => $name,
317             code => $coderef,
318             associated_interface => $meta,
319             );
320             }
321             }
322            
323             sub test_implementation
324             {
325 8     8 0 8396 my ($meta, $instance) = @_;
326 8 50 33     263 confess("Parameter is not an object that implements the interface; died")
327             unless blessed($instance) && $instance->DOES($meta->name);
328            
329 12         492 my @cases = map {
330 8 50       1995 $_->can('test_cases') ? @{$_->test_cases} : ()
  12         596  
331             } $meta->calculate_all_roles;
332            
333 8         15 my (@failed, @passed);
334 8         14 foreach my $case (@cases)
335             {
336 36 100       274 $case->test_instance($instance)
337             ? push(@passed, $case)
338             : push(@failed, $case)
339             }
340            
341 8         124 return 'MooseX::Interface::Meta::TestReport'->new(
342             failed => \@failed,
343             passed => \@passed,
344             );
345             }
346            
347             sub find_problematic_methods
348             {
349 10     10 0 19 my $meta = shift;
350 10         19 my @problems;
351            
352 10         128 foreach my $m ($meta->get_method_list)
353             {
354             # These shouldn't show up anyway.
355 12 50       5916 next if $m =~ qr(isa|can|DOES|VERSION|AUTHORITY);
356            
357 12         60 my $M = $meta->get_method($m);
358            
359             # skip Interface->meta (that's allowed!)
360 12 100       242 next if $M->isa('Moose::Meta::Method::Meta');
361            
362             # skip constants defined by constant.pm
363 2 50       18 next if $constant::declared{ $M->fully_qualified_name };
364            
365             # skip constants defined by MooseX::Interface
366 2 50       42 next if $M->isa('MooseX::Interface::Meta::Method::Constant');
367            
368 2         6 push @problems, $m;
369             }
370            
371 10         59 return @problems;
372             }
373              
374             sub find_problematic_method_modifiers
375             {
376 8     8 0 17 my $meta = shift;
377 8         18 my @problems;
378            
379 8         36 foreach my $type (qw( after around before override ))
380             {
381 32         80 my $has = "get_${type}_method_modifiers_map";
382 32         1541 my $map = $meta->$has;
383 32         288 foreach my $subname (sort keys %$map)
384             {
385 3 100 66     101 if (
386             $type eq 'before' &&
387             defined $meta->installed_modifiers->{$subname}
388             ) {
389             # It would be nice to check the refaddr of the
390             # modifier was the one we created, but Moose
391             # seems to wrap it or something.
392             #
393 2         9 next;
394             }
395 1         6 push @problems, "$type($subname)";
396             }
397             }
398            
399 8         40 return @problems;
400             }
401              
402             sub check_interface_integrity
403             {
404 10     10 0 19 my $meta = shift;
405            
406 10         45 my @checks = (
407             [ find_problematic_methods => 'Method' ],
408             [ find_problematic_method_modifiers => 'Method modifier' ],
409             );
410            
411 10 100       73 while (my ($check_method, $check_text) = @{ +shift(@checks) || [] })
  25         165  
412             {
413 18 100       69 if (my @problems = $meta->$check_method)
414             {
415 3         9 my $iface = $meta->name;
416 3         14 my $problems = Moose::Util::english_list(@problems);
417 3 50       41 my $s = (@problems==1 ? '' : 's');
418            
419 3         71 confess(
420             "${check_text}${s} defined within interface ${iface} ".
421             "(try Moose::Role instead): ${problems}; died"
422             );
423             }
424             }
425            
426 7         288 $meta->integrity_checked(1);
427             }
428             }
429              
430             1;
431              
432             __END__
433              
434             =head1 NAME
435              
436             MooseX::Interface - Java-style interfaces for Moose
437              
438             =head1 SYNOPSIS
439              
440             package DatabaseAPI::ReadOnly
441             {
442             use MooseX::Interface;
443             requires 'select';
444             one;
445             }
446            
447             package DatabaseAPI::ReadWrite
448             {
449             use MooseX::Interface;
450             extends 'DatabaseAPI::ReadOnly';
451             requires 'insert';
452             requires 'update';
453             requires 'delete';
454             one;
455             }
456            
457             package Database::MySQL
458             {
459             use Moose;
460             with 'DatabaseAPI::ReadWrite';
461             sub insert { ... }
462             sub select { ... }
463             sub update { ... }
464             sub delete { ... }
465             }
466            
467             Database::MySQL::->DOES('DatabaseAPI::ReadOnly'); # true
468             Database::MySQL::->DOES('DatabaseAPI::ReadWrite'); # true
469              
470             =head1 DESCRIPTION
471              
472             MooseX::Interface provides something similar to the concept of interfaces
473             as found in many object-oriented programming languages like Java and PHP.
474              
475             "What?!" I hear you cry, "can't this already be done in Moose using roles?"
476              
477             Indeed it can, and that's precisely how MooseX::Interface works. Interfaces
478             are just roles with a few additional restrictions:
479              
480             =over
481              
482             =item * You may not define any methods within an interface, except:
483              
484             =over
485              
486             =item * Moose's built-in C<meta> method, which will be defined for you;
487              
488             =item * You may override methods from L<UNIVERSAL>; and
489              
490             =item * You may define constants using the L<constant> pragma.
491              
492             =back
493              
494             =item * You may not define any attributes. (Attributes generate methods.)
495              
496             =item * You may not define method modifiers.
497              
498             =item * You can extend other interfaces, not normal roles.
499              
500             =back
501              
502             =head2 Functions
503              
504             =over
505              
506             =item C<< extends $interface >>
507              
508             Extends an existing interface.
509              
510             Yes, the terminology "extends" is used rather than "with".
511              
512             =item C<< excludes $role >>
513              
514             Prevents classes that implement this interface from also composing with
515             this role.
516              
517             =item C<< requires $method >>
518              
519             The name of a method (or attribute) that any classes implementing this
520             interface I<must> provide.
521              
522             =item C<< requires $method => \@signature >>
523              
524             Declares a signature for the given method. This effectively creates an
525             C<around> method modifier for the method to check the signature.
526              
527             As an example:
528              
529             requires log_message => [qw( Str )];
530              
531             If the C<log_message> method above were called with multiple arguments,
532             then the additional arguments would be tolerated; the only check is that
533             the first argument is a string.
534              
535             =item C<< const $name => $value >>
536              
537             Experimental syntactic sugar for declaring constants. It's probably not a
538             good idea to use this yet.
539              
540             =item C<< test_case { BLOCK } $name >>
541              
542             Experimental syntactic sugar for embedded test cases. This extends the idea
543             that an interface is a contract for classes to fulfil.
544              
545             The block will be called with an instance of a class claiming to implement
546             the interface in C<< $_ >> and should return true if the instance passes the
547             test and false if it fails.
548              
549             package CalculatorAPI
550             {
551             use MooseX::Interface;
552            
553             requires 'add';
554             test_case { $_->add(8, 2) == 10 };
555            
556             requires 'subtract';
557             test_case { $_->subtract(8, 2) == 6 };
558            
559             requires 'multiply';
560             test_case { $_->multiply(8, 2) == 16 };
561            
562             requires 'divide';
563             test_case { $_->divide(8, 2) == 4 };
564             }
565            
566             package Calculator
567             {
568             use Moose;
569             with 'CalculatorAPI';
570             sub add { $_[1] + $_[2] }
571             sub subtract { $_[1] - $_[2] }
572             sub multiply { $_[1] * $_[2] }
573             sub divide { $_[1] / $_[2] }
574             }
575            
576             my $result = CalculatorAPI->meta->test_implementation(
577             Calculator->new,
578             );
579              
580             The result of C<test_implementation> is an overloaded object which indicates
581             success when evaluated in boolean context; indicates the number of
582             failures in numeric context; and provides TAP-like "ok" or "not ok" in
583             string context. You can call methods C<passed> and C<failed> on this object
584             to return arrayrefs of failed test cases. Each test case is itself an
585             object, with C<name>, C<code> and C<associated_interface> attributes.
586              
587             Do not rely on test cases being run in any particular order, or maintaining
588             any state between test cases. (Theoretically each test case could be run with
589             a separate instance of the implementing class.)
590              
591             =item C<< one >>
592              
593             This function checks the integrity of your role, making sure it doesn't do
594             anything that interfaces are not supposed to do, like defining methods.
595              
596             While you don't need to call this function at all, your interface's integrity
597             will get checked anyway when classes implement the interface, so calling
598             C<one> will help you catch potential problems sooner. C<one> helpfully returns
599             '1', so it can be used as the magical return value at the end of a Perl
600             module.
601              
602             (Backwards compatibility note: in MooseX::Interface versions 0.005 and below,
603             this was performed automatically using L<Hook::AfterRuntime>. From 0.006, the
604             C<one> function was introduced instead.)
605              
606             =back
607              
608             =begin private
609              
610             =item C<< init_meta >>
611              
612             =end private
613              
614             =head1 BUGS
615              
616             Please report any bugs to
617             L<http://rt.cpan.org/Dist/Display.html?Queue=MooseX-Interface>.
618              
619             =head1 SEE ALSO
620              
621             L<MooseX::Interface::Tutorial>,
622             L<MooseX::Interface::Internals>.
623              
624             L<Moose::Role>, L<MooseX::ABCD>.
625              
626             =head1 AUTHOR
627              
628             Toby Inkster E<lt>tobyink@cpan.orgE<gt>.
629              
630             =head1 COPYRIGHT AND LICENCE
631              
632             This software is copyright (c) 2012 by Toby Inkster.
633              
634             This is free software; you can redistribute it and/or modify it under
635             the same terms as the Perl 5 programming language system itself.
636              
637             =head1 DISCLAIMER OF WARRANTIES
638              
639             THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED
640             WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
641             MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
642