File Coverage

blib/lib/Contract/Declare.pm
Criterion Covered Total %
statement 76 77 98.7
branch 20 24 83.3
condition 5 6 83.3
subroutine 15 15 100.0
pod 4 4 100.0
total 120 126 95.2


line stmt bran cond sub pod time code
1             package Contract::Declare;
2              
3 8     8   1028418 use v5.14;
  8         31  
4 8     8   48 use Exporter 'import';
  8         17  
  8         318  
5 8     8   4595 use Role::Tiny ();
  8         47607  
  8         298  
6 8     8   58 use Scalar::Util qw(blessed);
  8         21  
  8         492  
7 8     8   52 use Carp;
  8         14  
  8         4494  
8              
9              
10             our @EXPORT = qw(contract interface method returns);
11              
12             our $CURRENT_PKG;
13             my %REGISTRY;
14              
15             our $VERSION = '1.0.0';
16              
17              
18             sub contract {
19 11     11 1 29 local $CURRENT_PKG;
20 11         23 my $block;
21 11         38 ($CURRENT_PKG, $block) = @_;
22 11         50 $block->();
23 6         42 _build_contract($CURRENT_PKG, $REGISTRY{$CURRENT_PKG});
24 6 50       55 delete $REGISTRY{$CURRENT_PKG} unless $ENV{CONTRACT_DECLARE_KEEP_CONTRACT};
25             }
26              
27 11     11 1 1589568 sub interface (&) { shift }
28 12     12 1 194 sub returns { [ @_ ] }
29              
30             sub method {
31 12     12 1 48 my ($name, @parts) = @_;
32 12         29 my (@in, $out);
33              
34 12         34 for my $p (@parts) {
35 21 100       90 if (ref($p) eq 'ARRAY') {
36 12         26 $out = $p;
37 12         30 last;
38             }
39 9         22 push @in, $p;
40             }
41              
42 12         32 for my $arg (@in) {
43 9 100 66     566 croak "Contract violation: input argument for method '$name' must be an object with 'compiled_check'"
44             unless blessed($arg) && $arg->can('compiled_check');
45             }
46              
47 10 50       48 if ($out) {
48 10 50       63 croak "Contract violation: return type for method '$name' must be an arrayref"
49             unless ref($out) eq 'ARRAY';
50              
51 10         28 for my $ret (@$out) {
52 11 100 100     603 croak "Contract violation: each return type for method '$name' must be an object with 'compiled_check'"
53             unless blessed($ret) && $ret->can('compiled_check');
54             }
55             }
56              
57 7         86 $REGISTRY{$CURRENT_PKG}{$name} = [ \@in, $out ];
58             }
59              
60 0         0 sub _build_contract {
61 6     6   56 my ($pkg, $contract) = @_;
62              
63 8     8   65 no strict 'refs';
  8         48  
  8         4050  
64              
65 6         55 *{"${pkg}::new"} = sub {
66 5     5   100 my ($class, $impl) = @_;
67 5         12 my %cache;
68              
69 5         18 for my $method (keys %$contract) {
70 6         65 my $code = $impl->can($method);
71 6 100       255 croak "Contract violation: Implementation does not provide method '$method' for interface '$pkg'" unless $code;
72 5         16 $cache{$method} = $code;
73             }
74              
75             bless {
76 4         24 _impl => $impl,
77             _cache => \%cache,
78             }, $pkg;
79 6         82 };
80              
81 6         33 for my $method (keys %$contract) {
82 7         16 my ($in_rules, $out_rules) = @{$contract->{$method}};
  7         127  
83 7         26 my @in_checks = map { $_->compiled_check } @$in_rules;
  4         20  
84 7         33 my @out_checks = map { $_->compiled_check } @$out_rules;
  8         42  
85              
86 7         89 *{"${pkg}::$method"} = sub {
87 7     7   888 my ($self, @args) = @_;
88              
89 7         40 _validate(\@args, \@in_checks, "$pkg\::$method args");
90              
91 5         111 my @res = $self->{_cache}{$method}->($self->{_impl}, @args);
92              
93 5         44 _validate(\@res, \@out_checks, "$pkg\::$method return");
94              
95 4 100       50 return wantarray ? @res : $res[0];
96 7         149 };
97             }
98              
99 6         98 Role::Tiny->make_role($pkg);
100 6         996 $Role::Tiny::INFO{$pkg}{requires} = [ sort keys %$contract ];
101              
102 8     8   60 use strict 'refs';
  8         15  
  8         2659  
103             }
104              
105             sub _validate {
106 12     12   32 my ($values, $checkers, $label) = @_;
107              
108 12 100       53 return if @$checkers == 0;
109              
110 9 100       41 if (@$values != @$checkers) {
111 1         163 croak "Contract violation in $label: expected " . scalar(@$checkers) . " argument(s), got " . scalar(@$values);
112             }
113              
114 8         113 for (my $i = 0; $i < @$checkers; $i++) {
115 10 100       44 next if $checkers->[$i]->($values->[$i]);
116 2 50       22 my $val = defined($values->[$i]) ? "'$values->[$i]'" : 'undef';
117 2         474 croak "Contract violation in $label: argument #$i ($val) failed type check";
118             }
119             }
120              
121             1;
122              
123             __END__