package FormValidator::Simple;
use strict;
use base qw/Class::Accessor::Fast Class::Data::Inheritable Class::Data::Accessor/;
use Class::Inspector;
use UNIVERSAL::require;
use Scalar::Util qw/blessed/;
use FormValidator::Simple::Results;
use FormValidator::Simple::Exception;
use FormValidator::Simple::Data;
use FormValidator::Simple::Profile;
use FormValidator::Simple::Validator;
use FormValidator::Simple::Constants;
use FormValidator::Simple::Messages;
our $VERSION = '0.29';
__PACKAGE__->mk_classaccessors(qw/data prof results/);
__PACKAGE__->mk_classaccessor( messages => FormValidator::Simple::Messages->new );
sub import {
my $class = shift;
foreach my $plugin (@_) {
my $plugin_class;
if ($plugin =~ /^\+(.*)/) {
$plugin_class = $1;
} else {
$plugin_class = "FormValidator::Simple::Plugin::$plugin";
}
$class->load_plugin($plugin_class);
}
}
sub load_plugin {
my ($proto, $plugin) = @_;
my $class = ref $proto || $proto;
unless (Class::Inspector->installed($plugin)) {
FormValidator::Simple::Exception->throw(
qq/$plugin isn't installed./
);
}
$plugin->require;
if ($@) {
FormValidator::Simple::Exception->throw(
qq/Couldn't require "$plugin", "$@"./
);
}
{
no strict 'refs';
push @FormValidator::Simple::Validator::ISA, $plugin;
}
}
sub set_option {
my $class = shift;
while ( my ($key, $val) = splice @_, 0, 2 ) {
FormValidator::Simple::Validator->options->{$key} = $val;
}
}
sub set_messages {
my ($proto, $file) = @_;
my $class = ref $proto || $proto;
if (blessed $proto) {
$proto->messages(FormValidator::Simple::Messages->new)->load($file);
if ($proto->results) {
$proto->results->message($proto->messages);
} else {
$proto->results( FormValidator::Simple::Results->new(
messages => $proto->messages,
) );
}
} else {
$class->messages->load($file);
}
}
sub set_message_decode_from {
my ($self, $decode_from) = @_;
$self->messages->decode_from($decode_from);
}
sub set_message_format {
my ($proto, $format) = @_;
$format ||= '';
$proto->messages->format($format);
}
sub new {
my $proto = shift;
my $class = ref $proto || $proto;
my $self = bless { }, $class;
$self->_init(@_);
return $self;
}
sub _init {
my ($self, @args) = @_;
my $class = ref $self;
$class->set_option(@args);
$self->results( FormValidator::Simple::Results->new(
messages => $self->messages,
) );
}
sub set_invalid {
my ($self, $name, $type) = @_;
unless (ref $self) {
FormValidator::Simple::Exception->throw(
qq/set_invalid is instance method./
);
}
unless ($name && $type) {
FormValidator::Simple::Exception->throw(
qq/set_invalid needs two arguments./
);
}
$self->results->set_result($name, $type, FALSE);
}
sub check {
my ($proto, $input, $prof, $options) = @_;
$options ||= {};
my $self = blessed $proto ? $proto : $proto->new(%$options);
my $data = FormValidator::Simple::Data->new($input);
my $prof_setting = FormValidator::Simple::Profile->new($prof);
my $profile_iterator = $prof_setting->iterator;
PROFILE:
while ( my $profile = $profile_iterator->next ) {
my $name = $profile->name;
my $keys = $profile->keys;
my $constraints = $profile->constraints;
my $params = $data->param($keys);
$self->results->register($name);
$self->results->record($name)->data( @$params == 1 ? $params->[0] : '');
my $constraint_iterator = $constraints->iterator;
if ( scalar @$params == 1 ) {
unless ( defined $params->[0] && $params->[0] ne '' ) {
if ( $constraints->needs_blank_check ) {
$self->results->record($name)->is_blank( TRUE );
}
next PROFILE;
}
}
CONSTRAINT:
while ( my $constraint = $constraint_iterator->next ) {
my ($result, $data) = $constraint->check($params);
$self->results->set_result($name, $constraint->name, $result);
$self->results->record($name)->data($data) if $data;
}
}
return $self->results;
}
1;
=head1 NAME
FormValidator::Simple - validation with simple chains of constraints
=head1 SYNOPSIS
my $query = CGI->new;
$query->param( param1 => 'ABCD' );
$query->param( param2 => 12345 );
$query->param( mail1 => 'lyo.kato@gmail.com' );
$query->param( mail2 => 'lyo.kato@gmail.com' );
$query->param( year => 2005 );
$query->param( month => 11 );
$query->param( day => 27 );
my $result = FormValidator::Simple->check( $query => [
param1 => ['NOT_BLANK', 'ASCII', ['LENGTH', 2, 5]],
param2 => ['NOT_BLANK', 'INT' ],
mail1 => ['NOT_BLANK', 'EMAIL_LOOSE'],
mail2 => ['NOT_BLANK', 'EMAIL_LOOSE'],
{ mails => ['mail1', 'mail2' ] } => ['DUPLICATION'],
{ date => ['year', 'month', 'day'] } => ['DATE'],
] );
if ( $result->has_error ) {
my $tt = Template->new({ INCLUDE_PATH => './tmpl' });
$tt->process('template.html', { result => $result });
}
template example
[% IF result.has_error %]
<p>Found Input Error</p>
<ul>
[% IF result.missing('param1') %]
<li>param1 is blank.</li>
[% END %]
[% IF result.invalid('param1') %]
<li>param1 is invalid.</li>
[% END %]
[% IF result.invalid('param1', 'ASCII') %]
<li>param1 needs ascii code.</li>
[% END %]
[% IF result.invalid('param1', 'LENGTH') %]
<li>input into param1 with characters that's length should be between two and five. </li>
[% END %]
</ul>
[% END %]
example2
[% IF result.has_error %]
<ul>
[% FOREACH key IN result.error %]
[% FOREACH type IN result.error(key) %]
<li>invalid: [% key %] - [% type %]</li>
[% END %]
[% END %]
</ul>
[% END %]
=head1 DESCRIPTION
This module provides you a sweet way of form data validation with simple constraints chains.
You can write constraints on single line for each input data.
This idea is based on Sledge::Plugin::Validator, and most of validation code is borrowed from this plugin.
(Sledge is a MVC web application framework: http://sl.edge.jp [Japanese] )
The result object this module returns behaves like L<Data::FormValidator::Results>.
=head1 HOW TO SET PROFILE
FormValidator::Simple->check( $q => [
#profile
] );
Use 'check' method.
A hash reference includes input data, or an object of some class that has a method named 'param', for example L<CGI>, is needed as first argument.
And set profile as array reference into second argument. Profile consists of some pairs of input data and constraints.
my $q = CGI->new;
$q->param( param1 => 'hoge' );
FormValidator::Simple->check( $q => [
param1 => [ ['NOT_BLANK'], ['LENGTH', 4, 10] ],
] );
In this case, param1 is the name of a form element. and the array ref "[ ['NOT_BLANK']... ]" is a constraints chain.
Write constraints chain as arrayref, and you can set some constraints into it. In the last example, two constraints
'NOT_BLANK', and 'LENGTH' are set. Each constraints is should be set as arrayref, but in case the constraint has no
argument, it can be written as scalar text.
FormValidator::Simple->check( $q => [
param1 => [ 'NOT_BLANK', ['LENGTH', 4, 10] ],
] );
Now, in this sample 'NOT_BLANK' constraint is not an arrayref, but 'LENGTH' isn't. Because 'LENGTH' has two arguments, 4 and 10.
=head2 MULTIPLE DATA VALIDATION
When you want to check about multiple input data, do like this.
my $q = CGI->new;
$q->param( mail1 => 'lyo.kato@gmail.com' );
$q->param( mail2 => 'lyo.kato@gmail.com' );
my $result = FormValidator::Simple->check( $q => [
{ mails => ['mail1', 'mail2'] } => [ 'DUPLICATION' ],
] )
[% IF result.invalid('mails') %]
<p>mail1 and mail2 aren't same.</p>
[% END %]
and here's an another example.
my $q = CGI->new;
$q->param( year => 2005 );
$q->param( month => 12 );
$q->param( day => 27 );
my $result = FormValidator::Simple->check( $q => [
{ date => ['year', 'month', 'day'] } => [ 'DATE' ],
] );
[% IF result.invalid('date') %]
<p>Set correct date.</p>
[% END %]
=head2 FLEXIBLE VALIDATION
my $valid = FormValidator::Simple->new();
$valid->check( $q => [
param1 => [qw/NOT_BLANK ASCII/, [qw/LENGTH 4 10/] ],
] );
$valid->check( $q => [
param2 => [qw/NOT_BLANK/],
] );
my $results = $valid->results;
if ( found some error... ) {
$results->set_invalid('param3' => 'MY_ERROR');
}
template example
[% IF results.invalid('param1') %]
...
[% END %]
[% IF results.invalid('param2') %]
...
[% END %]
[% IF results.invalid('param3', 'MY_ERROR') %]
...
[% END %]
=head1 HOW TO SET OPTIONS
Option setting is needed by some validation, especially in plugins.
You can set them in two ways.
FormValidator::Simple->set_option(
dbic_base_class => 'MyProj::Model::DBIC',
charset => 'euc',
);
or
$valid = FormValidator::Simple->new(
dbic_base_class => 'MyProj::Model::DBIC',
charset => 'euc',
);
$valid->check(...)
=head1 VALIDATION COMMANDS
You can use follow variety validations.
and each validations can be used as negative validation with 'NOT_' prefix.
FormValidator::Simple->check( $q => [
param1 => [ 'INT', ['LENGTH', 4, 10] ],
param2 => [ 'NOT_INT', ['NOT_LENGTH', 4, 10] ],
] );
=over 4
=item SP
check if the data has space or not.
=item INT
check if the data is integer or not.
=item UINT
unsigined integer check.
for example, if -1234 is input, the validation judges it invalid.
=item DECIMAL
$q->param( 'num1' => '123.45678' );
my $result = FormValidator::Simple->check( $q => [
num1 => [ ['DECIMAL', 3, 5] ],
] );
each numbers (3,5) mean maximum digits before/after '.'
=item ASCII
check is the data consists of only ascii code.
=item LENGTH
check the length of the data.
my $result = FormValidator::Simple->check( $q => [
param1 => [ ['LENGTH', 4] ],
] );
check if the length of the data is 4 or not.
my $result = FormValidator::Simple->check( $q => [
param1 => [ ['LENGTH', 4, 10] ],
] );
when you set two arguments, it checks if the length of data is in
the range between 4 and 10.
=item HTTP_URL
verify it is a http(s)-url
my $result = FormValidator::Simple->check( $q => [
param1 => [ 'HTTP_URL' ],
] );
=item SELECTED_AT_LEAST
verify the quantity of selected parameters is counted over allowed minimum.
<input type="checkbox" name="hobby" value="music" /> Music
<input type="checkbox" name="hobby" value="movie" /> Movie
<input type="checkbox" name="hobby" value="game" /> Game
my $result = FormValidator::Simple->check( $q => [
hobby => ['NOT_BLANK', ['SELECTED_AT_LEAST', 2] ],
] );
=item REGEX
check with regular expression.
my $result = FormValidator::Simple->check( $q => [
param1 => [ ['REGEX', qr/^hoge$/ ] ],
] );
=item DUPLICATION
check if the two data are same or not.
my $result = FormValidator::Simple->check( $q => [
{ duplication_check => ['param1', 'param2'] } => [ 'DUPLICATION' ],
] );
=item EMAIL
check with L<Email::Valid>.
=item EMAIL_MX
check with L<Email::Valid>, including mx check.
=item EMAIL_LOOSE
check with L<Email::Valid::Loose>.
=item EMAIL_LOOSE_MX
check with L<Email::Valid::Loose>, including mx check.
=item DATE
check with L<Date::Calc>
my $result = FormValidator::Simple->check( $q => [
{ date => [qw/year month day/] } => [ 'DATE' ]
] );
=item TIME
check with L<Date::Calc>
my $result = FormValidator::Simple->check( $q => [
{ time => [qw/hour min sec/] } => ['TIME'],
] );
=item DATETIME
check with L<Date::Calc>
my $result = FormValidator::Simple->check( $q => [
{ datetime => [qw/year month day hour min sec/] } => ['DATETIME']
] );
=item DATETIME_STRPTIME
check with L<DateTime::Format::Strptime>.
my $q = CGI->new;
$q->param( datetime => '2006-04-26T19:09:21+0900' );
my $result = FormValidator::Simple->check( $q => [
datetime => [ [ 'DATETIME_STRPTIME', '%Y-%m-%dT%T%z' ] ],
] );
=item DATETIME_FORMAT
check with DateTime::Format::***. for example, L<DateTime::Format::HTTP>,
L<DateTime::Format::Mail>, L<DateTime::Format::MySQL> and etc.
my $q = CGI->new;
$q->param( datetime => '2004-04-26 19:09:21' );
my $result = FormValidator::Simple->check( $q => [
datetime => [ [qw/DATETIME_FORMAT MySQL/] ],
] );
=item GREATER_THAN
numeric comparison
my $result = FormValidator::Simple->check( $q => [
age => [ ['GREATER_THAN', 25] ],
] );
=item LESS_THAN
numeric comparison
my $result = FormValidator::Simple->check( $q => [
age => [ ['LESS_THAN', 25] ],
] );
=item EQUAL_TO
numeric comparison
my $result = FormValidator::Simple->check( $q => [
age => [ ['EQUAL_TO', 25] ],
] );
=item BETWEEN
numeric comparison
my $result = FormValidator::Simple->check( $q => [
age => [ ['BETWEEN', 20, 25] ],
] );
=item ANY
check if there is not blank data in multiple data.
my $result = FormValidator::Simple->check( $q => [
{ some_data => [qw/param1 param2 param3/] } => ['ANY']
] );
=item IN_ARRAY
check if the food ordered is in menu
my $result = FormValidator::Simple->check( $q => [
food => [ ['IN_ARRAY', qw/noodle soba spaghetti/] ],
] };
=back
=head1 HOW TO LOAD PLUGINS
use FormValidator::Simple qw/Japanese CreditCard/;
L<FormValidator::Simple::Plugin::Japanese>, L<FormValidator::Simple::Plugin::CreditCard> are loaded.
or use 'load_plugin' method.
use FormValidator::Simple;
FormValidator::Simple->load_plugin('FormValidator::Simple::Plugin::CreditCard');
If you want to load plugin which name isn't in FormValidator::Simple::Plugin namespace, use +.
use FormValidator::Simple qw/+MyApp::ValidatorPlugin/;
=head1 MESSAGE HANDLING
You can custom your own message with key and type.
[% IF result.has_error %]
[% FOREACH key IN result.error %]
[% FOREACH type IN result.error(key) %]
<p>error message:[% type %] - [% key %]</p>
[% END %]
[% END %]
[% END %]
And you can also set messages configuration before.
You can prepare configuration as hash reference.
FormValidator::Simple->set_messages( {
action1 => {
name => {
NOT_BLANK => 'input name!',
LENGTH => 'input name (length should be between 0 and 10)!',
},
email => {
DEFAULT => 'input correct email address!',
},
},
} );
or a YAML file.
# messages.yml
DEFAULT:
name:
DEFAULT: name is invalid!
action1:
name:
NOT_BLANK: input name!
LENGTH: input name(length should be between 0 and 10)!
email:
DEFAULT: input correct email address!
action2:
name:
DEFAULT: ...
# in your perl-script, set the file's path.
FormValidator::Simple->set_messages('messages.yml');
DEFAULT is a special type.
If it can't find setting for indicated validation-type, it uses message set for DEFAULT.
after setting, execute check(),
my $result = FormValidator::Simple->check( $q => [
name => [qw/NOT_BLANK/, [qw/LENGTH 0 10/] ],
email => [qw/NOT_BLANK EMAIL_LOOSE/, [qw/LENGTH 0 20/] ],
] );
# matching result and messages for indicated action.
my $messages = $result->messages('action1');
foreach my $message ( @$messages ) {
print $message, "\n";
}
# or you can get messages as hash style.
# each fieldname is the key
my $field_messages = $result->field_messages('action1');
if ($field_messages->{name}) {
foreach my $message ( @{ $field_messages->{name} } ) {
print $message, "\n";
}
}
When it can't find indicated action, name, and type, it searches proper message from DEFAULT action.
If in template file,
[% IF result.has_error %]
[% FOREACH msg IN result.messages('action1') %]
<p>[% msg %]</p>
[% END %]
[% END %]
you can set each message format.
FormValidator::Simple->set_message_format('<p>%s</p>');
my $result = FormValidator::Simple->check( $q => [
...profile
] );
[% IF result.has_error %]
[% result.messages('action1').join("\n") %]
[% END %]
=head1 RESULT HANDLING
See L<FormValidator::Simple::Results>
=head1 FLAGGED UTF-8
If you set encoding like follows, it automatically decode the
result messages.
FormValidtor::Simple->set_mesasges_decode_from('utf-8');
=head1 SEE ALSO
L<Data::FormValidator>
http://sl.edge.jp/ (Japanese)
http://sourceforge.jp/projects/sledge
=head1 AUTHOR
Lyo Kato E<lt>lyo.kato@gmail.comE<gt>
=head1 COPYRIGHT AND LICENSE
This library is free software.
You can redistribute it and/or modify it under the same terms as perl itself.
=cut