File Coverage

blib/lib/kura.pm
Criterion Covered Total %
statement 85 86 98.8
branch 31 34 91.1
condition 4 7 57.1
subroutine 28 29 96.5
pod 0 2 0.0
total 148 158 93.6


line stmt bran cond sub pod time code
1             package kura;
2 6     6   1701364 use strict;
  6         13  
  6         220  
3 6     6   35 use warnings;
  6         11  
  6         425  
4              
5             our $VERSION = "0.10";
6              
7 6     6   34 use Carp ();
  6         39  
  6         113  
8 6     6   2167 use Sub::Util ();
  6         1398  
  6         117  
9 6     6   31 use Scalar::Util ();
  6         8  
  6         5387  
10              
11             my %FORBIDDEN_NAME = map { $_ => 1 } qw{
12             BEGIN CHECK DESTROY END INIT UNITCHECK
13             AUTOLOAD STDIN STDOUT STDERR ARGV ARGVOUT ENV INC SIG
14             };
15              
16             my @ALLOWED_CONSTRAINT_CLASSES = qw(
17             Data::Validator
18             Poz::Types
19             );
20              
21             sub import {
22 19     19   17164 my $pkg = shift;
23 19         42 my $caller = caller;
24              
25 19         75 $pkg->import_into($caller, @_);
26             }
27              
28             # Import into the caller package.
29             sub import_into {
30 21     21 0 925 my $pkg = shift;
31 21         46 my ($caller, $name, $constraint) = @_;
32              
33 21         54 my ($kura_item, $err) = _new_kura_item($caller, $name, $constraint);
34 21 100       1182 Carp::croak $err if $err;
35              
36 14         36 _save_kura_item($kura_item, $caller);
37 14         26 _save_inc($caller);
38             }
39              
40             # Create a constraint object.
41             #
42             # @param $constraint Defined. Following `create_constraint` function allows these types: Object, CodeRef, HashRef.
43             # @param $opts Dict[name => Str, caller => Str]
44             # @return ($constraint, undef) | (undef, $error_message)
45             #
46             # NOTE: This function is a hook point. If you want to customize the constraint object, you can override this function.
47             sub create_constraint {
48 15     15 0 30 my ($constraint, $opts) = @_;
49              
50 15 100       59 if (my $blessed = Scalar::Util::blessed($constraint)) {
    100          
51 9 100       48 return _create_constraint_from_typetiny($constraint, $opts) if $constraint->isa('Type::Tiny');
52 7 100       42 return ($constraint, undef) if $constraint->can('check');
53 1 50       3 return ($constraint, undef) if grep { $constraint->isa($_) } @ALLOWED_CONSTRAINT_CLASSES;
  2         15  
54 1         7 return (undef, "Invalid constraint. Object must have a `check` method or allowed constraint class: $blessed");
55             }
56             elsif (my $reftype = Scalar::Util::reftype($constraint)) {
57 5 100       16 if ($reftype eq 'CODE') {
    50          
58 4         9 return _create_constraint_from_coderef($constraint, $opts);
59             }
60             elsif ($reftype eq 'HASH') {
61 1         2 return _create_constraint_from_hashref($constraint, $opts);
62             }
63             }
64              
65 1         4 return (undef, 'Invalid constraint');
66             }
67              
68             # Create a constraint object from a Type::Tiny object.
69             sub _create_constraint_from_typetiny {
70 2     2   17 my ($type, $opts) = @_;
71              
72 2 100       16 $type->{name} = $opts->{name} if $type->is_anon;
73              
74 2         17 return ($type, undef);
75             }
76              
77             # Create a constraint object from a code reference.
78             sub _create_constraint_from_coderef {
79 4     4   9 my ($coderef, $opts) = @_;
80              
81 4         19 require Type::Tiny;
82              
83 4         6 my $args = {};
84 4         7 $args->{name} = $opts->{name};
85 4     10   13 $args->{constraint} = sub { !!eval { $coderef->($_[0]) } };
  10         11965  
  10         53  
86 4     0   17 $args->{message} = sub { sprintf('%s did not pass the constraint "%s"', Type::Tiny::_dd($_[0]), $args->{name}) };
  0         0  
87              
88 4         24 return (Type::Tiny->new(%$args), undef);
89             }
90              
91             # Create a constraint object from a hash reference.
92             sub _create_constraint_from_hashref {
93 1     1   1 my ($args, $opts) = @_;
94              
95 1   50     5 my $blessed = delete $args->{blessed} || 'Type::Tiny';
96 1 50       62 eval "require $blessed" or die $@;
97              
98 1   33     7 $args->{name} //= $opts->{name};
99              
100 1         3 return ($blessed->new(%$args), undef);
101             }
102              
103             # Create a new kura item which is Dict[name => Str, code => CodeRef].
104             # If the name or constraint is invalid, it returns (undef, $error_message).
105             # Otherwise, it returns ($kura_item, undef).
106             sub _new_kura_item {
107 21     21   39 my ($caller, $name, $constraint) = @_;
108              
109             {
110 21 100       31 return (undef, 'name is required') if !defined $name;
  21         67  
111 20 100       63 return (undef, "'$name' is forbidden.") if $FORBIDDEN_NAME{$name};
112 19 100       217 return (undef, "'$name' is already defined") if $caller->can($name);
113             }
114              
115 18 100       44 return (undef, 'constraint is required') if !defined $constraint;
116 17         71 ($constraint, my $err) = create_constraint($constraint, { name => $name, caller => $caller });
117 17 100       2524 return (undef, $err) if $err;
118              
119             # Prefix '_' means private, so it is not exported.
120 14 100       48 my $is_private = $name =~ /^_/ ? 1 : 0;
121              
122 14     7   74 my $kura_item = { name => $name, code => sub { $constraint }, is_private => $is_private };
  29     7   1178574  
        7      
        20      
        7      
        7      
        29      
        9      
        9      
        9      
        9      
123 14         42 return ($kura_item, undef);
124             }
125              
126             # Save the kura item to the caller package
127             sub _save_kura_item {
128 14     14   25 my ($kura_item, $caller) = @_;
129              
130 14         26 my $name = $kura_item->{name};
131 14         118 my $code = Sub::Util::set_subname("$caller\::$name", $kura_item->{code});
132              
133 6     6   40 no strict "refs";
  6         9  
  6         298  
134 6     6   27 no warnings "once";
  6         16  
  6         1470  
135 14         26 *{"$caller\::$name"} = $code;
  14         68  
136              
137 14 100       38 if (!$kura_item->{is_private}) {
138 13         15 push @{"$caller\::EXPORT_OK"}, $name;
  13         71  
139 13         19 push @{"$caller\::KURA"}, $name;
  13         35  
140             }
141              
142 14         29 return;
143             }
144              
145             # Hack to make the caller package already loaded. Useful for multi-packages in a single file.
146             sub _save_inc {
147 14     14   21 my ($caller) = @_;
148              
149 14         34 ( my $file = $caller ) =~ s{::}{/}g;
150 14   100     114 $INC{"$file.pm"} ||= __FILE__;
151              
152 14         3550 return;
153             }
154              
155             1;
156             __END__