File Coverage

blib/lib/Hash/Match.pm
Criterion Covered Total %
statement 79 82 96.3
branch 30 32 93.7
condition 4 6 66.6
subroutine 20 20 100.0
pod 1 1 100.0
total 134 141 95.0


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   770 use v5.10.0;
  1         3  
6              
7 1     1   5 use strict;
  1         1  
  1         19  
8 1     1   5 use warnings;
  1         1  
  1         38  
9              
10             our $VERSION = 'v0.7.2';
11              
12 1     1   4 use Carp qw/ croak /;
  1         2  
  1         61  
13 1     1   568 use List::AllUtils qw/ natatime /;
  1         16078  
  1         83  
14 1     1   566 use Ref::Util qw/ is_coderef is_hashref is_ref is_regexpref /;
  1         2228  
  1         92  
15              
16             # RECOMMEND PREREQ: List::SomeUtils::XS
17             # RECOMMEND PREREQ: Ref::Util::XS
18              
19 1     1   461 use namespace::autoclean;
  1         13307  
  1         4  
20              
21              
22             sub new {
23 34     34 1 62732 my ($class, %args) = @_;
24              
25 34 50       96 if (my $rules = $args{rules}) {
26              
27 34 100       87 my $root = is_hashref($rules) ? '-all' : '-any';
28 34         70 my $self = _compile_rule( $root => $rules, $class );
29 31         107 bless $self, $class;
30              
31             } else {
32              
33 0         0 croak "Missing 'rules' attribute";
34              
35             }
36             }
37              
38             sub _compile_match {
39 56     56   88 my ($value) = @_;
40              
41 56 100       85 if ( is_ref($value) ) {
42              
43 19 100 50 22   67 return sub { ($_[0] // '') =~ $value } if is_regexpref($value);
  22         221  
44              
45 3 100   5   12 return sub { $value->($_[0]) } if is_coderef($value);
  5         11  
46              
47 1         14 croak sprintf('Unsupported type: \'%s\'', ref $value);
48              
49             } else {
50              
51 37 100 100 100   142 return sub { ($_[0] // '') eq $value } if (defined $value);
  100         561  
52              
53 2     3   6 return sub { !defined $_[0] };
  3         9  
54              
55             }
56             }
57              
58             my %KEY2FN = (
59             '-all' => List::AllUtils->can('all'),
60             '-and' => List::AllUtils->can('all'),
61             '-any' => List::AllUtils->can('any'),
62             '-notall' => List::AllUtils->can('notall'),
63             '-notany' => List::AllUtils->can('none'),
64             '-or' => List::AllUtils->can('any'),
65             );
66              
67             sub _key2fn {
68 60     60   89 my ($key, $ctx) = @_;
69              
70             # TODO: eventually add a warning message about -not being
71             # deprecated.
72              
73 60 100       101 if ($key eq '-not') {
74 3   50     7 $ctx //= '';
75 3 100       9 $key = ($ctx eq 'HASH') ? '-notall' : '-notany';
76             }
77              
78 60 100       155 $KEY2FN{$key} or croak "Unsupported key: '${key}'";
79             }
80              
81             sub _compile_rule {
82 113     113   184 my ( $key, $value, $ctx ) = @_;
83              
84 113 100       180 if ( my $key_ref = ( ref $key ) ) {
85              
86 8 100       17 if (is_regexpref($key)) {
    50          
87              
88 6         11 my $match = _compile_match($value);
89              
90 5         9 my $fn = _key2fn($ctx);
91              
92             return sub {
93 13     13   17 my $hash = $_[0];
94 19         34 $fn->( sub { $match->( $hash->{$_} ) },
95 13         23 grep { $_ =~ $key } (keys %{$hash}) );
  26         123  
  13         38  
96 5         24 };
97              
98             } elsif (is_coderef($key)) {
99              
100 2         4 my $match = _compile_match($value);
101              
102 2         4 my $fn = _key2fn($ctx);
103              
104             return sub {
105 6     6   9 my $hash = $_[0];
106 8         29 $fn->( sub { $match->( $hash->{$_} ) },
107 6         12 grep { $key->($_) } (keys %{$hash}) );
  11         71  
  6         19  
108 2         10 };
109              
110             } else {
111              
112 0         0 croak "Unsupported key type: '${key_ref}'";
113              
114             }
115              
116             } else {
117              
118 105         142 my $match_ref = ref $value;
119              
120 105 100       377 if ( $match_ref =~ /^(?:ARRAY|HASH)$/ ) {
    100          
121              
122             my $it = ( $match_ref eq 'ARRAY' )
123 22         107 ? natatime 2, @{$value}
124 56 100   77   150 : sub { each %{$value} };
  77         87  
  77         242  
125              
126 56         68 my @codes;
127 56         131 while ( my ( $k, $v ) = $it->() ) {
128 79         149 push @codes, _compile_rule( $k, $v, $key );
129             }
130              
131 53         85 my $fn = _key2fn($key, $match_ref);
132              
133             return sub {
134 199     199   12414 my $hash = $_[0];
135 199         677 $fn->( sub { $_->($hash) }, @codes );
  258         409  
136 52         247 };
137              
138             } elsif ( $match_ref =~ /^(?:Regexp|CODE|)$/ ) {
139              
140 48         84 my $match = _compile_match($value);
141              
142             return sub {
143 164     164   189 my $hash = $_[0];
144 164 100       519 (exists $hash->{$key}) ? $match->($hash->{$key}) : 0;
145 48         212 };
146              
147             } else {
148              
149 1         12 croak "Unsupported type: '${match_ref}'";
150              
151             }
152              
153             }
154              
155 0           croak "Unhandled condition";
156             }
157              
158             1;
159              
160             __END__