File Coverage

blib/lib/Hash/Match.pm
Criterion Covered Total %
statement 75 78 96.1
branch 30 32 93.7
condition 15 16 93.7
subroutine 19 19 100.0
pod 1 1 100.0
total 140 146 95.8


line stmt bran cond sub pod time code
1             package Hash::Match;
2              
3             # ABSTRACT: match contents of a hash against rules
4              
5 1     1   688 use v5.14;
  1         3  
6 1     1   4 use warnings;
  1         2  
  1         33  
7              
8             our $VERSION = 'v0.8.0';
9              
10 1     1   4 use Carp qw/ croak /;
  1         2  
  1         42  
11 1     1   528 use List::AllUtils qw/ natatime /;
  1         14306  
  1         91  
12 1     1   396 use Ref::Util qw/ is_arrayref is_blessed_ref is_coderef is_hashref is_ref is_regexpref /;
  1         1434  
  1         87  
13              
14             # RECOMMEND PREREQ: List::SomeUtils::XS
15             # RECOMMEND PREREQ: Ref::Util::XS
16              
17 1     1   391 use namespace::autoclean;
  1         9443  
  1         3  
18              
19              
20             sub new {
21 34     34 1 59605 my ($class, %args) = @_;
22              
23 34 50       74 if (my $rules = $args{rules}) {
24              
25 34 100       67 my $root = is_hashref($rules) ? '-all' : '-any';
26 34         57 my $self = _compile_rule( $root => $rules, $class );
27 31         90 bless $self, $class;
28              
29             } else {
30              
31 0         0 croak "Missing 'rules' attribute";
32              
33             }
34             }
35              
36             sub _compile_match {
37 56     56   69 my ($value) = @_;
38              
39 56 100       81 if ( is_ref($value) ) {
40              
41 19 100 50 23   54 return sub { ($_[0] // '') =~ $value } if is_regexpref($value);
  23         181  
42              
43 3 100   5   11 return sub { $value->($_[0]) } if is_coderef($value);
  5         11  
44              
45 1         31 croak sprintf('Unsupported type: \'%s\'', ref $value);
46              
47             } else {
48              
49 37 100 100 97   120 return sub { ($_[0] // '') eq $value } if (defined $value);
  97         548  
50              
51 2     3   7 return sub { !defined $_[0] };
  3         22  
52              
53             }
54             }
55              
56             sub _key2fn {
57 60     60   82 my ($key, $is_hash) = @_;
58              
59 60         86 state $KEY2FN = {
60             '-all' => List::AllUtils->can('all'),
61             '-and' => List::AllUtils->can('all'),
62             '-any' => List::AllUtils->can('any'),
63             '-notall' => List::AllUtils->can('notall'),
64             '-notany' => List::AllUtils->can('none'),
65             '-or' => List::AllUtils->can('any'),
66             };
67              
68             # TODO: eventually add a warning message about -not being
69             # deprecated.
70              
71 60 100       99 if ($key eq '-not') {
72 3 100       5 $key = $is_hash ? '-notall' : '-notany';
73             }
74              
75 60 100       129 $KEY2FN->{$key} or croak "Unsupported key: '${key}'";
76             }
77              
78             sub _compile_rule {
79 113     113   160 my ( $key, $value, $ctx ) = @_;
80              
81 113 100       162 if ( is_ref($key) ) {
82              
83 8 100       17 if (is_regexpref($key)) {
    50          
84              
85 6         14 my $match = _compile_match($value);
86              
87 5         12 my $fn = _key2fn($ctx);
88              
89             return sub {
90 13     13   14 my $hash = $_[0];
91 18         29 $fn->( sub { $match->( $hash->{$_} ) },
92 13         28 grep { $_ =~ $key } (keys %{$hash}) );
  26         134  
  13         35  
93 5         26 };
94              
95             } elsif (is_coderef($key)) {
96              
97 2         5 my $match = _compile_match($value);
98              
99 2         5 my $fn = _key2fn($ctx);
100              
101             return sub {
102 6     6   7 my $hash = $_[0];
103 9         26 $fn->( sub { $match->( $hash->{$_} ) },
104 6         13 grep { $key->($_) } (keys %{$hash}) );
  11         34  
  6         17  
105 2         21 };
106              
107             } else {
108              
109 0         0 croak sprintf( "Unsupported key type: '\%s'", ref $key );
110              
111             }
112              
113             } else {
114              
115 105 100 100     441 if ( !is_blessed_ref($value) && ( is_arrayref($value) || is_hashref($value) ) ) {
    100 100        
      100        
      100        
116              
117             my $it = is_arrayref($value)
118 22         130 ? natatime 2, @{$value}
119 56 100   77   119 : sub { each %{$value} };
  77         79  
  77         226  
120              
121 56         73 my @codes;
122 56         117 while ( my ( $k, $v ) = $it->() ) {
123 79         148 push @codes, _compile_rule( $k, $v, $key );
124             }
125              
126 53         85 my $fn = _key2fn($key, is_hashref($value));
127              
128             return sub {
129 201     201   11977 my $hash = $_[0];
130 201         664 $fn->( sub { $_->($hash) }, @codes );
  260         415  
131 52         236 };
132              
133             } elsif ( is_coderef($value) || is_regexpref($value) || !is_ref($value) ) {
134              
135 48         69 my $match = _compile_match($value);
136              
137             return sub {
138 164     164   169 my $hash = $_[0];
139 164 100       486 (exists $hash->{$key}) ? $match->($hash->{$key}) : 0;
140 48         211 };
141              
142             } else {
143              
144 1         42 croak sprintf( "Unsupported type: '\%s'", ref $value );
145              
146             }
147              
148             }
149              
150 0           croak "Unhandled condition";
151             }
152              
153             1;
154              
155             __END__