File Coverage

blib/lib/MooseX/Interface.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1 5     5   178261 use 5.010;
  5         22  
  5         207  
2 5     5   29 use strict;
  5         12  
  5         167  
3 5     5   27 use warnings;
  5         12  
  5         283  
4 5     5   5292 use utf8;
  5         50  
  5         29  
5 5     5   8139 use Moose::Exporter 0 ();
  0            
  0            
6             use Moose::Role 2.00 ();
7             use Moose::Util 0 ();
8             use Moose::Util::MetaRole 0 ();
9             use constant 1.01 ();
10             use Class::Load 0 ();
11              
12             {
13             package MooseX::Interface;
14            
15             BEGIN {
16             $MooseX::Interface::AUTHORITY = 'cpan:TOBYINK';
17             $MooseX::Interface::VERSION = '0.008';
18            
19             *requires = \&Moose::Role::requires;
20             *excludes = \&Moose::Role::excludes;
21             }
22            
23             sub test_case (&;$)
24             {
25             Class::MOP::class_of( (scalar caller)[0] )->add_test_case(@_);
26             }
27            
28             sub const
29             {
30             my ($meta, $name, $value) = @_;
31             $meta->add_constant($name, $value);
32             }
33            
34             sub extends
35             {
36             my ($meta, $other) = @_;
37             Class::Load::load_class($other);
38             confess("Tried to extent $other, but $other is not an interface; died")
39             unless $other->meta->can('is_interface') && $other->meta->is_interface;
40             Moose::Util::ensure_all_roles($meta->name, $other);
41             }
42            
43             sub one ()
44             {
45             my $meta = shift || Class::MOP::class_of( (scalar caller)[0] );
46             $meta->check_interface_integrity;
47             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             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             goto $import;
67             }
68              
69             sub init_meta
70             {
71             my $class = shift;
72             my %options = @_;
73            
74             my $iface = $options{for_class};
75             Moose::Role->init_meta(%options);
76            
77             Moose::Util::MetaRole::apply_metaroles(
78             for => $iface,
79             role_metaroles => {
80             role => ['MooseX::Interface::Trait::Role'],
81             }
82             );
83            
84             Class::MOP::class_of($iface)->is_interface(1);
85             }
86             }
87              
88             {
89             package MooseX::Interface::Meta::Method::Constant;
90             use Moose;
91             extends 'Moose::Meta::Method';
92            
93             BEGIN {
94             $MooseX::Interface::Meta::Method::Constant::AUTHORITY = 'cpan:TOBYINK';
95             $MooseX::Interface::Meta::Method::Constant::VERSION = '0.008';
96             }
97             }
98              
99             {
100             package MooseX::Interface::Meta::Method::Required;
101             use Moose;
102             extends 'Moose::Meta::Role::Method::Required';
103            
104             BEGIN {
105             $MooseX::Interface::Meta::Method::Required::AUTHORITY = 'cpan:TOBYINK';
106             $MooseX::Interface::Meta::Method::Required::VERSION = '0.008';
107             }
108             }
109              
110             {
111             package MooseX::Interface::Meta::Method::Required::WithSignature;
112             use Moose;
113             use Moose::Util::TypeConstraints ();
114             extends 'MooseX::Interface::Meta::Method::Required';
115            
116             BEGIN {
117             $MooseX::Interface::Meta::Method::Required::WithSignature::AUTHORITY = 'cpan:TOBYINK';
118             $MooseX::Interface::Meta::Method::Required::WithSignature::VERSION = '0.008';
119             }
120            
121             has signature => (
122             is => 'ro',
123             isa => 'ArrayRef',
124             required => 1,
125             );
126            
127             sub check_signature
128             {
129             my ($meta, $args) = @_;
130             my $sig = $meta->signature;
131            
132             for my $i (0 .. $#{$sig})
133             {
134             my $tc = Moose::Util::TypeConstraints::find_type_constraint($sig->[$i]);
135             return 0 unless $tc->check($args->[$i]);
136             }
137            
138             return 1;
139             }
140             }
141              
142             {
143             package MooseX::Interface::Meta::TestReport;
144             use Moose;
145             use namespace::clean;
146            
147             BEGIN {
148             $MooseX::Interface::Meta::TestReport::AUTHORITY = 'cpan:TOBYINK';
149             $MooseX::Interface::Meta::TestReport::VERSION = '0.008';
150             }
151            
152             use overload
153             q[bool] => sub { my $self = shift; !scalar(@{ $self->failed }) },
154             q[0+] => sub { my $self = shift; scalar(@{ $self->failed }) },
155             q[""] => sub { my $self = shift; scalar(@{ $self->failed }) ? 'not ok' : 'ok' },
156             q[@{}] => sub { my $self = shift; $self->failed },
157             fallback => 1,
158             ;
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             use Moose;
170             use namespace::clean;
171            
172             BEGIN {
173             $MooseX::Interface::Meta::TestCase::AUTHORITY = 'cpan:TOBYINK';
174             $MooseX::Interface::Meta::TestCase::VERSION = '0.008';
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             my ($self, $instance) = @_;
198             local $_ = $instance;
199             $self->code->(@_);
200             }
201             }
202              
203             {
204             package MooseX::Interface::Trait::Role;
205             use Moose::Role;
206             use namespace::clean;
207             use overload ();
208            
209             BEGIN {
210             $MooseX::Interface::Trait::Role::AUTHORITY = 'cpan:TOBYINK';
211             $MooseX::Interface::Trait::Role::VERSION = '0.008';
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             my ($meta, $name, $value) = @_;
296             $meta->add_method(
297             $name => 'MooseX::Interface::Meta::Method::Constant'->wrap(
298             sub () { $value },
299             name => $name,
300             package_name => $meta->name,
301             ),
302             );
303             }
304            
305             sub add_test_case
306             {
307             my ($meta, $coderef, $name) = @_;
308             if (blessed $coderef)
309             {
310             push @{ $meta->test_cases }, $coderef;
311             }
312             else
313             {
314             $name //= sprintf("%s test case %d", $meta->name, 1 + @{ $meta->test_cases });
315             push @{ $meta->test_cases }, 'MooseX::Interface::Meta::TestCase'->new(
316             name => $name,
317             code => $coderef,
318             associated_interface => $meta,
319             );
320             }
321             }
322            
323             sub test_implementation
324             {
325             my ($meta, $instance) = @_;
326             confess("Parameter is not an object that implements the interface; died")
327             unless blessed($instance) && $instance->DOES($meta->name);
328            
329             my @cases = map {
330             $_->can('test_cases') ? @{$_->test_cases} : ()
331             } $meta->calculate_all_roles;
332            
333             my (@failed, @passed);
334             foreach my $case (@cases)
335             {
336             $case->test_instance($instance)
337             ? push(@passed, $case)
338             : push(@failed, $case)
339             }
340            
341             return 'MooseX::Interface::Meta::TestReport'->new(
342             failed => \@failed,
343             passed => \@passed,
344             );
345             }
346            
347             sub find_problematic_methods
348             {
349             my $meta = shift;
350             my @problems;
351            
352             foreach my $m ($meta->get_method_list)
353             {
354             # These shouldn't show up anyway.
355             next if $m =~ qr(isa|can|DOES|VERSION|AUTHORITY);
356            
357             my $M = $meta->get_method($m);
358            
359             # skip Interface->meta (that's allowed!)
360             next if $M->isa('Moose::Meta::Method::Meta');
361            
362             # skip constants defined by constant.pm
363             next if $constant::declared{ $M->fully_qualified_name };
364            
365             # skip constants defined by MooseX::Interface
366             next if $M->isa('MooseX::Interface::Meta::Method::Constant');
367            
368             push @problems, $m;
369             }
370            
371             return @problems;
372             }
373              
374             sub find_problematic_method_modifiers
375             {
376             my $meta = shift;
377             my @problems;
378            
379             foreach my $type (qw( after around before override ))
380             {
381             my $has = "get_${type}_method_modifiers_map";
382             my $map = $meta->$has;
383             foreach my $subname (sort keys %$map)
384             {
385             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             next;
394             }
395             push @problems, "$type($subname)";
396             }
397             }
398            
399             return @problems;
400             }
401              
402             sub check_interface_integrity
403             {
404             my $meta = shift;
405            
406             my @checks = (
407             [ find_problematic_methods => 'Method' ],
408             [ find_problematic_method_modifiers => 'Method modifier' ],
409             );
410            
411             while (my ($check_method, $check_text) = @{ +shift(@checks) || [] })
412             {
413             if (my @problems = $meta->$check_method)
414             {
415             my $iface = $meta->name;
416             my $problems = Moose::Util::english_list(@problems);
417             my $s = (@problems==1 ? '' : 's');
418            
419             confess(
420             "${check_text}${s} defined within interface ${iface} ".
421             "(try Moose::Role instead): ${problems}; died"
422             );
423             }
424             }
425            
426             $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