File Coverage

blib/lib/MooseX/Role/Matcher.pm
Criterion Covered Total %
statement 59 59 100.0
branch 30 30 100.0
condition 6 6 100.0
subroutine 15 15 100.0
pod n/a
total 110 110 100.0


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2             package MooseX::Role::Matcher;
3             our $VERSION = '0.05';
4              
5 9     9   6695 use MooseX::Role::Parameterized;
  9         665071  
  9         37  
6 9     9   275048 use List::Util qw/first/;
  9         17  
  9         588  
7 9     9   3599 use List::MoreUtils qw/any all/;
  9         78785  
  9         52  
8              
9             =head1 NAME
10              
11             MooseX::Role::Matcher - generic object matching based on attributes and methods
12              
13             =head1 VERSION
14              
15             version 0.05
16              
17             =head1 SYNOPSIS
18              
19             package Person;
20             use Moose;
21             with 'MooseX::Role::Matcher' => { default_match => 'name' };
22              
23             has name => (is => 'ro', isa => 'Str');
24             has age => (is => 'ro', isa => 'Num');
25             has phone => (is => 'ro', isa => 'Str');
26              
27             package main;
28             my @people = (
29             Person->new(name => 'James', age => 22, phone => '555-1914'),
30             Person->new(name => 'Jesse', age => 22, phone => '555-6287'),
31             Person->new(name => 'Eric', age => 21, phone => '555-7634'),
32             );
33              
34             # is James 22?
35             $people[0]->match(age => 22);
36              
37             # which people are not 22?
38             my @not_twenty_two = Person->grep_matches([@people], '!age' => 22);
39              
40             # do any of the 22-year-olds have a phone number ending in 4?
41             Person->any_match([@people], age => 22, phone => qr/4$/);
42              
43             # does everyone's name start with either J or E?
44             Person->all_match([@people], name => [qr/^J/, qr/^E/]);
45              
46             # find the first person whose name is 4 characters long (using the
47             # default_match of name)
48             my $four = Person->first_match([@people], sub { length == 4 });
49              
50             =head1 DESCRIPTION
51              
52             This role adds flexible matching and searching capabilities to your Moose
53             class. It provides a match method, which tests attributes and methods of your
54             object against strings, regexes, or coderefs, and also provides several class
55             methods for using match on lists of objects.
56              
57             =head1 PARAMETERS
58              
59             MooseX::Role::Matcher is a parameterized role (see
60             L<MooseX::Role::Parameterized>). The parameters it takes are:
61              
62             =over
63              
64             =item default_match
65              
66             Which attribute/method to test against by default, if none are specified
67             explicitly. Setting default_match to 'foo' allows using
68             C<< $obj->match('bar') >> rather than C<< $obj->match(foo => 'bar') >>.
69              
70             =item allow_missing_methods
71              
72             If set to true, matching against a method that doesn't exist is treated as though matching against undef. Otherwise, the match call dies.
73              
74             =back
75              
76             =cut
77              
78             parameter default_match => (
79             isa => 'Str',
80             );
81              
82             parameter allow_missing_methods => (
83             isa => 'Bool',
84             );
85              
86             role {
87             my $p = shift;
88             my $default = $p->default_match;
89             my $allow_missing_methods = $p->allow_missing_methods;
90              
91             method _apply_to_matches => sub {
92 9     9   22 my $class = shift;
        87      
        36      
93 9         18 my $on_match = shift;
94 9         21 my @list = @{ shift() };
  9         67  
95 9         30 my @matchers = @_;
96 9     23   64 $on_match->(sub { $_->match(@matchers) }, @list);
  23         66  
97             };
98              
99             =head1 METHODS
100              
101             =head2 first_match
102              
103             my $four = Person->first_match([@people], sub { length == 4 });
104              
105             Class method which takes an arrayref of objects in the class that consumed this
106             role, and calls C<match> on each object in the arrayref, passing it the
107             remaining arguments, and returns the first object for which match returns true.
108              
109             =cut
110              
111             method first_match => sub {
112 3     3   39174 my $class = shift;
113 3         18 $class->_apply_to_matches(\&first, @_);
114             };
115              
116             =head2 grep_matches
117              
118             my @not_twenty_two = Person->grep_matches([@people], '!age' => 22);
119              
120             Class method which takes an arrayref of objects in the class that consumed this
121             role, and calls C<match> on each object in the arrayref, passing it the
122             remaining arguments, and returns the each object for which match returns true.
123              
124             =cut
125              
126             method grep_matches => sub {
127 2     2   17 my $class = shift;
128 2     2   12 my $grep = sub { my $code = shift; grep { $code->() } @_ };
  2         6  
  2         7  
  6         15  
129 2         10 $class->_apply_to_matches($grep, @_);
130             };
131              
132             =head2 any_match
133              
134             Person->any_match([@people], age => 22, number => qr/4$/);
135              
136             Class method which takes an arrayref of objects in the class that consumed this
137             role, and calls C<match> on each object in the arrayref, passing it the
138             remaining arguments, and returns true if any C<match> calls return true,
139             otherwise returns false.
140              
141             =cut
142              
143             method any_match => sub {
144 2     2   15 my $class = shift;
145 2         11 $class->_apply_to_matches(\&any, @_);
146             };
147              
148             =head2 all_match
149              
150             Person->all_match([@people], name => [qr/^J/, qr/^E/]);
151              
152             Class method which takes an arrayref of objects in the class that consumed this
153             role, and calls C<match> on each object in the arrayref, passing it the
154             remaining arguments, and returns false if any C<match> calls return false,
155             otherwise returns true.
156              
157             =cut
158              
159             method all_match => sub {
160 2     2   23 my $class = shift;
161 2         10 $class->_apply_to_matches(\&all, @_);
162             };
163              
164             method _match => sub {
165 112     112   162 my $self = shift;
166 112         186 my $value = shift;
167 112         151 my $seek = shift;
168              
169             # first check seek types that could match undef
170 112 100       445 if (!defined $seek) {
    100          
    100          
    100          
    100          
    100          
171 3         14 return !defined $value;
172             }
173             elsif (ref($seek) eq 'CODE') {
174 16         29 local $_ = $value;
175 16         39 return $seek->();
176             }
177             elsif (ref($seek) eq 'ARRAY') {
178 6         10 for (@$seek) {
179 9 100       21 return 1 if $self->_match($value => $_);
180             }
181 1         6 return 0;
182             }
183             # then bail out if we still have an undef value
184             elsif (!defined $value) {
185 2         8 return 0;
186             }
187             # and now check seek types that would error with an undef value
188             elsif (ref($seek) eq 'Regexp') {
189 24         119 return $value =~ $seek;
190             }
191             elsif (ref($seek) eq 'HASH') {
192 27 100 100     134 return 0 unless blessed($value) &&
193             $value->does('MooseX::Role::Matcher');
194 25         5287 return $value->match(%$seek);
195             }
196 34         108 return $value eq $seek;
197             };
198              
199             =head2 match
200              
201             $person->match(age => 22);
202              
203             This method provides the majority of the functionality of this role. It accepts
204             a hash of arguments, with keys being the methods (usually attributes) of the
205             object to be tested, and values being things to test against them. Possible
206             types of values are:
207              
208             =over
209              
210             =item SCALAR
211              
212             Returns true if the result of the method is equal to (C<eq>) the value of the
213             scalar, otherwise returns false.
214              
215             =item REGEXP
216              
217             Returns true if the result of the method matches the regexp, otherwise returns
218             false.
219              
220             =item CODEREF
221              
222             Calls the coderef with C<$_> set to the result of the method, returning true if
223             the coderef returns true, and false otherwise.
224              
225             =item UNDEF
226              
227             Returns true if the method returns undef, or if the object doesn't have a
228             method by this name, otherwise returns false.
229              
230             =item ARRAYREF
231              
232             Matches the result of the method against each element in the arrayref as
233             described above, returning true if any of the submatches return true, and false
234             otherwise.
235              
236             =item HASHREF
237              
238             If the method does not return an object which does MooseX::Role::Matcher,
239             returns false. Otherwise, returns the result of calling C<match> on the
240             returned object, with the contents of the hashref as arguments.
241              
242             =back
243              
244             Method names can also be given with a leading '!', which inverts that test. The first key can be omitted from the argument list if it is the method name passed to the default_match parameter when composing this role.
245              
246             =cut
247              
248             method match => sub {
249 78     129   70763 my $self = shift;
250 78 100       241 unshift @_, $default if @_ % 2 == 1;
251 78         215 my %args = @_;
252              
253             # All the conditions must be true for true to be returned. Return
254             # immediately if a false condition is found.
255 78         209 for my $matcher (keys %args) {
256 104         504 my ($invert, $name) = $matcher =~ /^(!)?(.*)$/;
257 104 100 100     654 confess blessed($self) . " has no method named $name"
258             unless $self->can($name) || $allow_missing_methods;
259 103 100       1878 my $value = $self->can($name) ? $self->$name : undef;
260 103         665 my $seek = $args{$matcher};
261              
262 103 100       232 my $matched = $self->_match($value => $seek) ? 1 : 0;
263              
264 103 100       436 if ($invert) {
265 10 100       53 return 0 if $matched;
266             }
267             else {
268 93 100       249 return 0 unless $matched;
269             }
270             }
271              
272 55         238 return 1;
273             };
274              
275             };
276              
277 9     9   12120 no MooseX::Role::Parameterized;
  9         21  
  9         61  
278              
279             =head1 AUTHOR
280              
281             Jesse Luehrs <doy at tozt dot net>
282              
283             =head1 COPYRIGHT AND LICENSE
284              
285             This software is copyright (c) 2008-2009 by Jesse Luehrs.
286              
287             This is free software; you can redistribute it and/or modify it under
288             the same terms as perl itself.
289              
290             =head1 TODO
291              
292             Better error handling/reporting
293              
294             =head1 SEE ALSO
295              
296             L<Moose>
297              
298             L<MooseX::Role::Parameterized>
299              
300             =head1 BUGS
301              
302             No known bugs.
303              
304             Please report any bugs through RT: email
305             C<bug-moosex-role-matcher at rt.cpan.org>, or browse to
306             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=MooseX-Role-Matcher>.
307              
308             =head1 SUPPORT
309              
310             You can find this documentation for this module with the perldoc command.
311              
312             perldoc MooseX::Role::Matcher
313              
314             You can also look for information at:
315              
316             =over 4
317              
318             =item * AnnoCPAN: Annotated CPAN documentation
319              
320             L<http://annocpan.org/dist/MooseX-Role-Matcher>
321              
322             =item * CPAN Ratings
323              
324             L<http://cpanratings.perl.org/d/MooseX-Role-Matcher>
325              
326             =item * RT: CPAN's request tracker
327              
328             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=MooseX-Role-Matcher>
329              
330             =item * Search CPAN
331              
332             L<http://search.cpan.org/dist/MooseX-Role-Matcher>
333              
334             =back
335              
336             =cut
337              
338             1;