package Valiant::Validator::Format; use Moo; use Valiant::I18N; with 'Valiant::Validator::Each'; has match => (is=>'ro', predicate=>'has_match'); has without => (is=>'ro', predicate=>'has_without'); has invalid_format_match => (is=>'ro', required=>1, default=>sub {_t 'invalid_format_match'}); has invalid_format_without => (is=>'ro', required=>1, default=>sub {_t 'invalid_format_without'}); has exclusion => (is=>'ro', required=>1, default=>sub {_t 'exclusion'}); sub BUILD { my ($self, $args) = @_; $self->_requires_one_of($args, 'match', 'without'); } # Stolen from Email::Valid our $RFC822PAT = <<'EOF'; [\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\ xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xf f\n\015()]*)*\)[\040\t]*)*(?:(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\x ff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|"[^\\\x80-\xff\n\015 "]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[\040\t]*(?:\([^\\\x80-\ xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80 -\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]* )*(?:\.[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\ \\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\ x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x8 0-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|"[^\\\x80-\xff\n \015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[\040\t]*(?:\([^\\\x 80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^ \x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040 \t]*)*)*@[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([ ^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\ \\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\ x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80- \xff\n\015\[\]]|\\[^\x80-\xff])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015() ]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\ x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:\.[\04 0\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\ n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\ 015()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?! [^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\ ]]|\\[^\x80-\xff])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\ x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\01 5()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*)*|(?:[^(\040)<>@,;:". \\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff] )|"[^\\\x80-\xff\n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[^ ()<>@,;:".\\\[\]\x80-\xff\000-\010\012-\037]*(?:(?:\([^\\\x80-\xff\n\0 15()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][ ^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)|"[^\\\x80-\xff\ n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015"]*)*")[^()<>@,;:".\\\[\]\ x80-\xff\000-\010\012-\037]*)*<[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(? :(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80- \xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:@[\040\t]* (?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015 ()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015() ]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\0 40)<>@,;:".\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\ [^\x80-\xff])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\ xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]* )*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x80 -\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x 80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t ]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\ \[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff]) *\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x 80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80 -\xff\n\015()]*)*\)[\040\t]*)*)*(?:,[\040\t]*(?:\([^\\\x80-\xff\n\015( )]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\ \x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*@[\040\t ]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\0 15()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015 ()]*)*\)[\040\t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^( \040)<>@,;:".\\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]| \\[^\x80-\xff])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80 -\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015() ]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x 80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^ \x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040 \t]*)*(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:". \\\[\]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff ])*\])[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\ \x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x 80-\xff\n\015()]*)*\)[\040\t]*)*)*)*:[\040\t]*(?:\([^\\\x80-\xff\n\015 ()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\ \\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*)?(?:[^ (\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000- \037\x80-\xff])|"[^\\\x80-\xff\n\015"]*(?:\\[^\x80-\xff][^\\\x80-\xff\ n\015"]*)*")[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]| \([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\)) [^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x80-\xff \n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\x ff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*( ?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\ 000-\037\x80-\xff])|"[^\\\x80-\xff\n\015"]*(?:\\[^\x80-\xff][^\\\x80-\ xff\n\015"]*)*")[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\x ff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*) *\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*)*@[\040\t]*(?:\([^\\\x80-\x ff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80- \xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*) *(?:[^(\040)<>@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\ ]\000-\037\x80-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\] )[\040\t]*(?:\([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80- \xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\x ff\n\015()]*)*\)[\040\t]*)*(?:\.[\040\t]*(?:\([^\\\x80-\xff\n\015()]*( ?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()]*(?:\\[^\x80-\xff][^\\\x80 -\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*)*\)[\040\t]*)*(?:[^(\040)< >@,;:".\\\[\]\000-\037\x80-\xff]+(?![^(\040)<>@,;:".\\\[\]\000-\037\x8 0-\xff])|\[(?:[^\\\x80-\xff\n\015\[\]]|\\[^\x80-\xff])*\])[\040\t]*(?: \([^\\\x80-\xff\n\015()]*(?:(?:\\[^\x80-\xff]|\([^\\\x80-\xff\n\015()] *(?:\\[^\x80-\xff][^\\\x80-\xff\n\015()]*)*\))[^\\\x80-\xff\n\015()]*) *\)[\040\t]*)*)*>) EOF $RFC822PAT =~ s/\n//g; # credit_card # domain_FQDN # IsISO8601 # uppercase # lowercase # is_url # IP address # us zip6 / us zip10 # us social security # our %prebuilt_formats = ( alpha => [ qr/^[a-z]+$/i, _t('not_alpha') ], words => [ qr/^[a-z ]+$/i, _t('not_words') ], # letters and spaces only alpha_numeric => [ qr/^\w+$/, _t('not_alpha_numeric') ], alphanumeric => [ qr/^\w+$/, _t('not_alpha_numeric') ], # likely common error email => [ qr/^$RFC822PAT$/o, _t('not_email') ], zip => [ qr/^\d{5}(?:[- ]\d{4})?$/, _t('not_zip') ], zip5 => [ qr/^\d\d\d\d\d$/, _t('not_zip5') ], zip9 => [ qr/^\d\d\d\d\d[- ]\d\d\d\d$/, _t('not_zip9') ], ascii => [qr/^\p{IsASCII}*\z/, _t('not_ascii') ], word => [qr/^\w*\z/, _t('not_word') ], ); sub prebuilt_formats { return \%prebuilt_formats } around BUILDARGS => sub { my ( $orig, $class, @args ) = @_; my $args = $class->$orig(@args); foreach my $key (qw/match without/) { next unless exists $args->{$key}; next if (ref($args->{$key})||'') eq 'Regexp'; next unless $class->prebuilt_formats->{$args->{$key}}; my $prebuilt = $args->{$key}; $args->{$key} = $class->prebuilt_formats->{$prebuilt}->[0]; $args->{message} = $class->prebuilt_formats->{$prebuilt}->[1] unless exists $args->{message}; return $args; } return $args; }; sub normalize_shortcut { my ($class, $arg) = @_; return +{ match => $arg }; } sub validate_each { my ($self, $record, $attribute, $value, $opts) = @_; if($self->has_match) { my $with = $self->_cb_value($record, $self->match); $record->errors->add($attribute, $self->invalid_format_match, $opts) unless defined($value) && $value =~m/$with/; } if($self->has_without) { my $with = $self->_cb_value($record, $self->without); if($value =~m/$with/) { $record->errors->add($attribute, $self->invalid_format_without, $opts); } } } 1; =head1 NAME Valiant::Validator::Format - Validate a value based on a regular expression =head1 SYNOPSIS package Local::Test::Format; use Moo; use Valiant::Validations; has phone => (is=>'ro'); has name => (is=>'ro'); has email => (is=>'ro'); validates phone => ( format => +{ match => qr/\d\d\d-\d\d\d-\d\d\d\d/, }, ); validates name => ( format => +{ without => qr/\d+/, }, ); validates email => format => 'email'; my $object = Local::Test::Format->new( phone => '387-1212', name => 'jjn1056', ); $object->validate; warn $object->errors->_dump; $VAR1 = { 'phone' => [ 'Phone does not match the required pattern' ], 'name' => [ 'Name contains invalid characters' ], 'email' => [ 'Email is not an email address' ], }; =head1 DESCRIPTION Validates that the attribute value either matches a given regular expression (C<match) or that it fails to match an exclusion expression (C<without>). Values that fail the C<match> condition (which can be a regular expression or a reference to a method that provides one) will add an error matching the tag C<invalid_format_match>, which can be overridden as with other tags. Values that match the C<without> conditions (also either a regular expression or a coderef that provides one) with all an error matching the tag C<invalid_format_without> which can also be overridden via a passed parameter. A number of format shortcuts are built in to help you bootstrap faster. Use the text string in place of the regexp (for example:) validates email => ( format => +{ match => 'email', } ); You can add to the prebuilts by adding keys to the global C<%prebuilt_formats> hash. See code for more. =over 4 =item email Uses a regexp to validate that a string looks like an email. This is naive but probably acceptable for most uses. For real detailed eail checking you'll need to build something on L<Email::Valid>. Uses translation tag C<not_email>. =item alpha Text can only be upper or lowercase letter. Uses translation tag C<not_alpha>. =item alpha_numeric Text can only be upper or lowercase letters, numbers or '_'. Uses translation tag C<not_alpha_numeric>. =item words Only letters and spaces. Error message default is translation tag C<not_words>. =item word Must contain only a single word. Adds error C<not_word> if fails. =item zip =item zip5 =item zip9 Match US zipcodes. This is a pattern match only, we don't actually check if the zip is a true valid zipcode, just that it looks like one. C<zip5> matches a common 5 digit zipcode and return C<not_zip5> translation tag if not. C<zip9> looks for the 'Zip +4' extended zipcode, such as 11111-2222 (the separator can be either '-' or a space). Adds errors C<not_zip9> if not. Finally C<zip> will match either zip or zip +4 and add error C<not_zip> on failure to match. =item ascii Must contain only ASCII characters. Adds error C<not_ascii> if fails. =cut =head1 SHORTCUT FORM This validator supports the follow shortcut forms: validates attribute => ( format => qr/\d\d\d/, ... ); Which is the same as: validates attribute => ( format => +{ match => qr/\d\d\d/, }, ); We choose to shortcut the 'match' pattern based on experiene that suggested it is more common to required a specific pattern than to have an exclusion pattern. This also works for the coderef form. validates attribute => ( format => \&pattern, ... ); Which is the same as: validates attribute => ( format => +{ match => \&pattern, }, ); And of course its very handy for using one of the built in pattern tags: validates email => (format => 'email'); Which is the same as: validates email => ( format => +{ match => 'email', } ); =head1 GLOBAL PARAMETERS This validator supports all the standard shared parameters: C<if>, C<unless>, C<message>, C<strict>, C<allow_undef>, C<allow_blank>. =head1 SEE ALSO L<Valiant>, L<Valiant::Validator>, L<Valiant::Validator::Each>. =head1 AUTHOR See L<Valiant> =head1 COPYRIGHT & LICENSE See L<Valiant> =cut