#!/usr/bin/perl package MooseX::Role::Matcher; use MooseX::Role::Parameterized; use List::Util qw/first/; use List::MoreUtils qw/any all/; =head1 NAME MooseX::Role::Matcher - generic object matching based on attributes and methods =head1 SYNOPSIS package Person; use Moose; with 'MooseX::Role::Matcher' => { default_match => 'name' }; has name => (is => 'ro', isa => 'Str'); has age => (is => 'ro', isa => 'Num'); has phone => (is => 'ro', isa => 'Str'); package main; my @people = ( Person->new(name => 'James', age => 22, phone => '555-1914'), Person->new(name => 'Jesse', age => 22, phone => '555-6287'), Person->new(name => 'Eric', age => 21, phone => '555-7634'), ); # is James 22? $people[0]->match(age => 22); # which people are not 22? my @not_twenty_two = Person->grep_matches([@people], '!age' => 22); # do any of the 22-year-olds have a phone number ending in 4? Person->any_match([@people], age => 22, phone => qr/4$/); # does everyone's name start with either J or E? Person->all_match([@people], name => [qr/^J/, qr/^E/]); # find the first person whose name is 4 characters long (using the # default_match of name) my $four = Person->first_match([@people], sub { length == 4 }); =head1 DESCRIPTION This role adds flexible matching and searching capabilities to your Moose class. It provides a match method, which tests attributes and methods of your object against strings, regexes, or coderefs, and also provides several class methods for using match on lists of objects. =head1 PARAMETERS MooseX::Role::Matcher is a parameterized role (see L). The parameters it takes are: =over =item default_match Which attribute/method to test against by default, if none are specified explicitly. Setting default_match to 'foo' allows using C<< $obj->match('bar') >> rather than C<< $obj->match(foo => 'bar') >>. =item allow_missing_methods If set to true, matching against a method that doesn't exist is treated as though matching against undef. Otherwise, the match call dies. =back =cut parameter default_match => ( isa => 'Str', ); parameter allow_missing_methods => ( isa => 'Bool', ); role { my $p = shift; my $default = $p->default_match; my $allow_missing_methods = $p->allow_missing_methods; method _apply_to_matches => sub { my $class = shift; my $on_match = shift; my @list = @{ shift() }; my @matchers = @_; $on_match->(sub { $_->match(@matchers) }, @list); }; =head1 METHODS =head2 first_match my $four = Person->first_match([@people], sub { length == 4 }); Class method which takes an arrayref of objects in the class that consumed this role, and calls C on each object in the arrayref, passing it the remaining arguments, and returns the first object for which match returns true. =cut method first_match => sub { my $class = shift; $class->_apply_to_matches(\&first, @_); }; =head2 grep_matches my @not_twenty_two = Person->grep_matches([@people], '!age' => 22); Class method which takes an arrayref of objects in the class that consumed this role, and calls C on each object in the arrayref, passing it the remaining arguments, and returns the each object for which match returns true. =cut method grep_matches => sub { my $class = shift; my $grep = sub { my $code = shift; grep { $code->() } @_ }; $class->_apply_to_matches($grep, @_); }; =head2 any_match Person->any_match([@people], age => 22, number => qr/4$/); Class method which takes an arrayref of objects in the class that consumed this role, and calls C on each object in the arrayref, passing it the remaining arguments, and returns true if any C calls return true, otherwise returns false. =cut method any_match => sub { my $class = shift; $class->_apply_to_matches(\&any, @_); }; =head2 all_match Person->all_match([@people], name => [qr/^J/, qr/^E/]); Class method which takes an arrayref of objects in the class that consumed this role, and calls C on each object in the arrayref, passing it the remaining arguments, and returns false if any C calls return false, otherwise returns true. =cut method all_match => sub { my $class = shift; $class->_apply_to_matches(\&all, @_); }; method _match => sub { my $self = shift; my $value = shift; my $seek = shift; # first check seek types that could match undef if (!defined $seek) { return !defined $value; } elsif (ref($seek) eq 'CODE') { local $_ = $value; return $seek->(); } elsif (ref($seek) eq 'ARRAY') { for (@$seek) { return 1 if $self->_match($value => $_); } return 0; } # then bail out if we still have an undef value elsif (!defined $value) { return 0; } # and now check seek types that would error with an undef value elsif (ref($seek) eq 'Regexp') { return $value =~ $seek; } elsif (ref($seek) eq 'HASH') { return 0 unless blessed($value) && $value->does('MooseX::Role::Matcher'); return $value->match(%$seek); } return $value eq $seek; }; =head2 match $person->match(age => 22); This method provides the majority of the functionality of this role. It accepts a hash of arguments, with keys being the methods (usually attributes) of the object to be tested, and values being things to test against them. Possible types of values are: =over =item SCALAR Returns true if the result of the method is equal to (C) the value of the scalar, otherwise returns false. =item REGEXP Returns true if the result of the method matches the regexp, otherwise returns false. =item CODEREF Calls the coderef with C<$_> set to the result of the method, returning true if the coderef returns true, and false otherwise. =item UNDEF Returns true if the method returns undef, or if the object doesn't have a method by this name, otherwise returns false. =item ARRAYREF Matches the result of the method against each element in the arrayref as described above, returning true if any of the submatches return true, and false otherwise. =item HASHREF If the method does not return an object which does MooseX::Role::Matcher, returns false. Otherwise, returns the result of calling C on the returned object, with the contents of the hashref as arguments. =back 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. =cut method match => sub { my $self = shift; unshift @_, $default if @_ % 2 == 1; my %args = @_; # All the conditions must be true for true to be returned. Return # immediately if a false condition is found. for my $matcher (keys %args) { my ($invert, $name) = $matcher =~ /^(!)?(.*)$/; confess blessed($self) . " has no method named $name" unless $self->can($name) || $allow_missing_methods; my $value = $self->can($name) ? $self->$name : undef; my $seek = $args{$matcher}; my $matched = $self->_match($value => $seek) ? 1 : 0; if ($invert) { return 0 if $matched; } else { return 0 unless $matched; } } return 1; }; }; no MooseX::Role::Parameterized; =head1 AUTHOR Jesse Luehrs =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2008-2009 by Jesse Luehrs. This is free software; you can redistribute it and/or modify it under the same terms as perl itself. =head1 TODO Better error handling/reporting =head1 SEE ALSO L L =head1 BUGS No known bugs. Please report any bugs through RT: email C, or browse to L. =head1 SUPPORT You can find this documentation for this module with the perldoc command. perldoc MooseX::Role::Matcher You can also look for information at: =over 4 =item * AnnoCPAN: Annotated CPAN documentation L =item * CPAN Ratings L =item * RT: CPAN's request tracker L =item * Search CPAN L =back =cut 1;