Validator::Custom - Validate user input easily


Validator-Custom documentation Contained in the Validator-Custom distribution.

Index


Code Index:

NAME

Top

Validator::Custom - Validate user input easily

SYNOPSYS

Top

    use Validator::Custom;
    my $vc = Validator::Custom->new;

    my $data = {age => 19, name => 'Ken Suzuki'};

    my $rule = [
        age => {message => 'age must be integer'} => [
            'not_blank',
            'int'
        ],
        name => {message => 'name must be string. the length 1 to 5'} => [
            'not_blank',
            {length => [1, 5]}
        ],
        price => [
            'not_blank',
            'int'
        ]
    ];

    my $result = $vc->validate($data, $rule);

    unless ($result->is_ok) {
        if ($result->has_missing) {
            my $missing_params = $result->missing_params;
        }

        if ($result->has_invalid) {
            my $messages = $result->messages_to_hash;
        }
    }
    my $valid_data = $result->data;
    my $raw_data = $result->raw_data;
    my $loose_data = $result->loose_data;

    


DESCRIPTION

Top

Validator::Custom validate user input easily. The features are the following ones.

GUIDE

Top

Validator::Custom::Guide - Validator::Custom Guide

ATTRIBUTES

Top

constraints

    my $constraints = $vc->constraints;
    $vc             = $vc->constraints(\%constraints);

Constraint functions.

data_filter

    my $filter = $vc->data_filter;
    $vc        = $vc->data_filter(\&data_filter);

Filter for input data. If data is not hash reference, you can convert the data to hash reference.

    $vc->data_filter(sub {
        my $data = shift;

        my $hash = {};

        # Convert data to hash reference

        return $hash;
    });

error_stock

    my $error_stock = $vc->error_stcok;
    $vc             = $vc->error_stock(1);

If error_stock is set to 0, validate() return soon after invalid value is found.

Default to 1.

rule

    my $rule = $vc->rule;
    $vc      = $vc->rule(\@rule);

Validation rule. If second argument of validate() is not specified. this rule is used.

syntax

    my $syntax = $vc->syntax;
    $vc        = $vc->syntax($syntax);

Syntax of rule.

METHODS

Top

Validator::Custom inherits all methods from Object::Simple and implements the following new ones.

new

    my $vc = Validator::Custom->new;

Create a new Validator::Custom object.

js_fill_form_button

    my $button = $self->js_fill_form_button(
        mail => '[abc]{3}@[abc]{2}.com,
        title => '[pqr]{5}'
    );

Create javascript button source code to fill form. You can specify string or pattern like regular expression.

If you click this button, each text box is filled with the specified pattern string, and checkbox, radio button, and list box is automatically selected.

Note that this methods require JSON module.

validate

    $result = $vc->validate($data, $rule);
    $result = $vc->validate($data);

Validate the data. Return value is Validator::Custom::Result object. If second argument is not specified, rule attribute is used.

register_constraint

    $vc->register_constraint(%constraint);
    $vc->register_constraint(\%constraint);

Register constraint function.

    $vc->register_constraint(
        int => sub {
            my $value    = shift;
            my $is_valid = $value =~ /^\-?[\d]+$/;
            return $is_valid;
        },
        ascii => sub {
            my $value    = shift;
            my $is_valid = $value =~ /^[\x21-\x7E]+$/;
            return $is_valid;
        }
    );

You can register filter function.

    $vc->register_constraint(
        trim => sub {
            my $value = shift;
            $value =~ s/^\s+//;
            $value =~ s/\s+$//;

            return [1, $value];
        }
    );

Filter function return array reference, first element is the value if the value is valid or not, second element is the converted value by filter function.

RULE SYNTAX

Top

Validation rule has the following syntax.

    # Rule syntax
    my $rule = [                              # 1 Rule is array ref
        key => [                              # 2 Constraints is array ref
            'constraint',                     # 3 Constraint is string
            {'constraint' => 'args'}          #     or hash ref (arguments)
            ['constraint', 'err'],            #     or arrya ref (message)
        ],
        key => [                           
            [{constraint => 'args'}, 'err']   # 4 With argument and message
        ],
        {key => ['key1', 'key2']} => [        # 5.1 Multi-parameters validation
            'constraint'
        ],
        {key => qr/^key/} => [                # 5.2 Multi-parameters validation
            'constraint'                              using regular expression
        ],
        key => [
            '@constraint'                     # 6 Multi-values validation
        ],
        key => {message => 'err', ... } => [  # 7 With option
            'constraint'
        ],
        key => [
            '!constraint'                     # 8 Negativate constraint
        ],
        key => [
            'constraint1 || constraint2'      # 9 "OR" condition constraint
        ],
    ];

Rule can have option, following options is available.

1. message
     {message => "Input right value"}

Message for invalid value.

2. default
    {default => 5}

Default value, set to data of Validator::Custom::Result when invalid value or missing value is found

3. copy
    {copy => 0}

If copy is 0, the value is not copied to data of Validator::Custom::Result.

Default to 1.

4. require
    {require => 0}

If require is 0, The value is not appended to missing parameter list even if the value is not found

Default to 1.

CONSTRAINTS

Top

ascii

    my $data => {name => 'Ken'};
    my $rule = [
        name => [
            'ascii'
        ]
    ];

Ascii.

between

    my $data = {age => 19};
    my $rule = [
        age => [
            {between => [1, 20]} # (1, 2, .. 19, 20)
        ]
    ];

Between A and B.

blank

    my $data = {name => ''};
    my $rule = [
        name => [
            'blank'
        ]
    ];

Blank.

decimal

    my $data = {num1 => '123.45678', num2 => '1.45'};
    my $rule => [
        num1 => [
            'decimal'
        ],
        num2 => [
            {'decimal' => [1, 2]}
        ]
    ];

Decimal. You can specify maximus digits number at before and after '.'.

defined

    my $data => {name => 'Ken'};
    my $rule = [
        name => [
            'defined'
        ]
    ];

Defined.

duplication

    my $data = {mail1 => 'a@somehost.com', mail2 => 'a@somehost.com'};
    my $rule => [
        [qw/mail1 mail2/] => [
            'duplication'
        ]
    ];

Check if the two data are same or not.

equal_to

    my $data = {price => 1000};
    my $rule = [
        price => [
            {'equal_to' => 1000}
        ]
    ];

Numeric equal comparison.

greater_than

    my $data = {price => 1000};
    my $rule = [
        price => [
            {'greater_than' => 900}
        ]
    ];

Numeric "greater than" comparison

http_url

    my $data = {url => 'http://somehost.com'};
    my $rule => [
        url => [
            'http_url'
        ]
    ];

HTTP(or HTTPS) URL.

int

    my $data = {age => 19};
    my $rule = [
        age => [
            'int'
        ]
    ];

Integer.

in_array

    my $data = {food => 'sushi'};
    my $rule = [
        food => [
            {'in_array' => [qw/sushi bread apple/]}
        ]
    ];

Check if the values is in array.

length

    my $data = {value1 => 'aaa', value2 => 'bbbbb'};
    my $rule => [
        value1 => [
            {'length' => 3}
        ],
        value2 => [
            {'length' => [2, 5]} # 'bb' to 'bbbbb'
        ]
    ];

Length of the value.

less_than

    my $data = {num => 20};
    my $rule = [
        num => [
            {'less_than' => 25}
        ]
    ];

Numeric "less than" comparison.

not_blank

    my $data = {name => 'Ken'};
    my $rule = [
        name => [
            'not_blank' # Except for ''
        ]
    ];

Not blank.

not_defined

    my $data = {name => 'Ken'};
    my $rule = [
        name => [
            'not_defined'
        ]
    ];

Not defined.

not_space

    my $data = {name => 'Ken'};
    my $rule = [
        name => [
            'not_space' # Except for '', ' ', '   '
        ]
    ];

Not contain only space characters.

space

    my $data = {name => '   '};
    my $rule = [
        name => [
            'space' # '', ' ', '   '
        ]
    ];

White space or empty stirng.

uint

    my $data = {age => 19};
    my $rule = [
        age => [
            'uint'
        ]
    ];

Unsigned integer.

regex

    my $data = {num => '123'};
    my $rule => [
        num => [
            {'regex' => qr/\d{0,3}/}
        ]
    ];

Match a regular expression.

selected_at_least

    my $data = {hobby => ['music', 'movie' ]};
    my $rule => [
        hobby => [
            {selected_at_least => 1}
        ]
    ];

Selected at least specified count item. In other word, the array contains at least specified count element.

FILTERS

Top

date_to_timepiece

    my $data = {date => '2010/11/12'};
    my $rule = [
        date => [
            'date_to_timepiece'
        ]
    ];

The value which looks like date is converted to Time::Piece object. If the value contains 8 digits, the value is assumed date.

    2010/11/12 # ok
    2010-11-12 # ok
    20101112   # ok
    2010       # NG
    2010111106 # NG

And year and month and mday combination is ok.

    my $data = {year => 2011, month => 3, mday => 9};
    my $rule = [
        {date => ['year', 'month', 'mday']} => [
            'date_to_timepiece'
        ]
    ];

Note that Time::Piece is required.

datetime_to_timepiece

    my $data = {datetime => '2010/11/12 12:14:45'};
    my $rule = [
        datetime => [
            'datetime_to_timepiece'
        ]
    ];

The value which looks like date and time is converted to Time::Piece object. If the value contains 14 digits, the value is assumed date and time.

    2010/11/12 12:14:45 # ok
    2010-11-12 12:14:45 # ok
    20101112 121445     # ok
    2010                # NG
    2010111106 12       # NG

And year and month and mday combination is ok.

    my $data = {year => 2011, month => 3, mday => 9
                hour => 10, min => 30, sec => 30};
    my $rule = [
        {datetime => ['year', 'month', 'mday', 'hour', 'min', 'sec']} => [
            'datetime_to_timepiece'
        ]
    ];

Note that Time::Piece is required.

merge

    my $data = {name1 => 'Ken', name2 => 'Rika', name3 => 'Taro'};
    my $rule = [
        {merged_name => ['name1', 'name2', 'name3']} => [
            'merge' # KenRikaTaro
        ]
    ];

Merge the values.

shift

    my $data = {names => ['Ken', 'Taro']};
    my $rule => [
        names => [
            'shift' # 'Ken'
        ]
    ];

Shift the head element of array.

to_array

    my $data = {languages => 'Japanese'};
    my $rule = [
        languages => [
            'to_array' # ['Japanese']
        ],
    ];

Convert non array reference data to array reference. This is useful to check checkbox values or select multiple values.

trim

    my $data = {name => '  Ken  '};
    my $rule = [
        name => [
            'trim' # 'Ken'
        ]
    ];

Trim leading and trailing white space.

trim_collapse

    my $data = {name => '  Ken   Takagi  '};
    my $rule = [
        name => [
            'trim_collapse' # 'Ken Takagi'
        ]
    ];

Trim leading and trailing white space, and collapse all whitespace characters into a single space.

trim_lead

    my $data = {name => '  Ken  '};
    my $rule = [
        name => [
            'trim_lead' # 'Ken  '
        ]
    ];

Trim leading white space.

trim_trail

    my $data = {name => '  Ken  '};
    my $rule = [
        name => [
            'trim_trail' # '  Ken'
        ]
    ];

Trim trailing white space.

DEPRECATED FUNCTIONALITIES

Top

Validator::Custom

    # Atrribute methods
    shared_rule # Removed at 2017/1/1

    # Methods
    __PACKAGE__->constraints(...); # Call constraints method as class method
                                   # Removed at 2017/1/1
L<Validator::Custom::Result>

    # Attribute methods
    error_infos # Removed at 2017/1/1 

    # Methods
    add_error_info # Removed at 2017/1/1
    error # Removed at 2017/1/1
    errors # Removed at 2017/1/1
    errors_to_hash # Removed at 2017/1/1
    invalid_keys # Removed at 2017/1/1
    remove_error_info# Removed at 2017/1/1

BACKWORD COMPATIBLE POLICY

Top

If a functionality is DEPRECATED, you can know it by DEPRECATED warnings except for attribute method. You can check all DEPRECATED functionalities by document. DEPRECATED functionality is removed after five years, but if at least one person use the functionality and tell me that thing I extend one year each time you tell me it.

EXPERIMENTAL functionality will be changed without warnings.

This policy is changed at 2011/6/28

AUTHOR

Top

Yuki Kimoto, <kimoto.yuki at gmail.com>

http://github.com/yuki-kimoto/Validator-Custom

COPYRIGHT & LICENCE

Top


Validator-Custom documentation Contained in the Validator-Custom distribution.

package Validator::Custom;
use Object::Simple -base;
use 5.008001;
our $VERSION = '0.1425';

use Carp 'croak';
use Validator::Custom::Constraint;
use Validator::Custom::Result;

has ['data_filter', 'rule'],
    error_stock => 1;

has syntax => <<'EOS';
### Syntax of validation rule
my $rule = [                              # 1 Rule is array ref
    key => [                              # 2 Constraints is array ref
        'constraint',                     # 3 Constraint is string
        {'constraint' => 'args'}          #     or hash ref (arguments)
        ['constraint', 'err'],            #     or arrya ref (message)
    ],
    key => [                           
        [{constraint => 'args'}, 'err']   # 4 With argument and message
    ],
    {key => ['key1', 'key2']} => [        # 5.1 Multi-parameters validation
        'constraint'
    ],
    {key => qr/^key/} => [                # 5.2 Multi-parameters validation
        'constraint'                              using regular expression
    ],
    key => [
        '@constraint'                     # 6 Multi-values validation
    ],
    key => {message => 'err', ... } => [  # 7 With options
        'constraint'
    ],
    key => [
        '!constraint'                     # 8 Negativate constraint
    ],
    key => [
        'constraint1 || constraint2'      # 9 "OR" condition
    ],
];

EOS

sub js_fill_form_button {
    my ($self, $rule) = @_;
    
    my $r = $self->_parse_random_string_rule($rule);
    
    require JSON;
    my $r_json = JSON->new->encode($r);
    
    my $javascript = << "EOS";
  (function () {

    var rule = $r_json;
  
    var create_random_value = function (rule, name) {
      var patterns = rule[name];
      if (patterns === undefined) {
        return "";
      }
      
      var value = "";
      for (var i = 0; i < patterns.length; i++) {
        var pattern = patterns[i];
        var num = Math.floor(Math.random() * pattern.length);
        value = value + pattern[num];
      }
      
      return value;
    };
    
  	var addEvent = (function(){
  	  if(document.addEventListener) {
  	    return function(node,type,handler){
  	      node.addEventListener(type,handler,false);
  	    };
  	  } else if (document.attachEvent) {
  	    return function(node,type,handler){
  	      node.attachEvent('on' + type, function(evt){
  	        handler.call(node, evt);
  	      });
  	    };
  	  }
  	})();
  	
  	var button = document.createElement("input");
  	button.setAttribute("type","button");
  	button.value = "Fill Form";
    document.body.insertBefore(button, document.body.firstChild)

    addEvent(
      button,
      "click",
      function () {
        
        var input_elems = document.getElementsByTagName('input');
        var radio_names = {};
        var checkbox_names = {};
        for (var i = 0; i < input_elems.length; i++) {
          var e = input_elems[i];

          var name = e.getAttribute("name");
          var type = e.getAttribute("type");
          if (type === "text" || type === "hidden" || type === "password") {
            var value = create_random_value(rule, name);
            e.value = value;
          }
          else if (type === "checkbox") {
            e.checked = Math.floor(Math.random() * 2) ? true : false;
          }
          else if (type === "radio") {
            radio_names[name] = 1;
          }
        }
        
        for (name in radio_names) {
          var elems = document.getElementsByName(name);
          var num = Math.floor(Math.random() * elems.length);
          elems[num].checked = true;
        }
        
        var textarea_elems = document.getElementsByTagName("textarea");
        for (var i = 0; i < textarea_elems.length; i++) {
          var e = textarea_elems[i];
          
          var name = e.getAttribute("name");
          var value = create_random_value(rule, name);
          
          var text = document.createTextNode(value);
          
          if (e.firstChild) {
            e.removeChild(e.firstChild);
          }
          
          e.appendChild(text);
        }
        
        var select_elems = document.getElementsByTagName("select");
        for (var i = 0; i < select_elems.length; i++) {
          var e = select_elems[i];
          var options = e.options;
          if (e.multiple) {
            for (var k = 0; k < options.length; k++) {
              options[k].selected = Math.floor(Math.random() * 2) ? true : false;
            }
          }
          else {
            var num = Math.floor(Math.random() * options.length);
            e.selectedIndex = num;
          }
        }
      }
    );
  })();
EOS

    return $javascript;
}

sub new {
    my $self = shift->SUPER::new(@_);

    $self->register_constraint(
        any               => sub { 1 },
        ascii             => \&Validator::Custom::Constraint::ascii,
        between           => \&Validator::Custom::Constraint::between,
        blank             => \&Validator::Custom::Constraint::blank,
        date_to_timepiece => \&Validator::Custom::Constraint::date_to_timepiece,
        datetime_to_timepiece => \&Validator::Custom::Constraint::datetime_to_timepiece,
        decimal           => \&Validator::Custom::Constraint::decimal,
        defined           => sub { defined $_[0] },
        duplication       => \&Validator::Custom::Constraint::duplication,
        equal_to          => \&Validator::Custom::Constraint::equal_to,
        greater_than      => \&Validator::Custom::Constraint::greater_than,
        http_url          => \&Validator::Custom::Constraint::http_url,
        int               => \&Validator::Custom::Constraint::int,
        in_array          => \&Validator::Custom::Constraint::in_array,
        length            => \&Validator::Custom::Constraint::length,
        less_than         => \&Validator::Custom::Constraint::less_than,
        merge             => \&Validator::Custom::Constraint::merge,
        not_defined       => \&Validator::Custom::Constraint::not_defined,
        not_space         => \&Validator::Custom::Constraint::not_space,
        not_blank         => \&Validator::Custom::Constraint::not_blank,
        uint              => \&Validator::Custom::Constraint::uint,
        regex             => \&Validator::Custom::Constraint::regex,
        selected_at_least => \&Validator::Custom::Constraint::selected_at_least,
        shift             => \&Validator::Custom::Constraint::shift_array,
        space             => \&Validator::Custom::Constraint::space,
        to_array          => \&Validator::Custom::Constraint::to_array,
        trim              => \&Validator::Custom::Constraint::trim,
        trim_collapse     => \&Validator::Custom::Constraint::trim_collapse,
        trim_lead         => \&Validator::Custom::Constraint::trim_lead,
        trim_trail        => \&Validator::Custom::Constraint::trim_trail
    );
    
    return $self;
}

sub register_constraint {
    my $self = shift;
    
    # Merge
    my $constraints = ref $_[0] eq 'HASH' ? $_[0] : {@_};
    $self->constraints({%{$self->constraints}, %$constraints});
    
    return $self;
}

our %VALID_OPTIONS = map {$_ => 1} qw/message default copy require/;

sub validate {
    my ($self, $data, $rule) = @_;
    
    # Class
    my $class = ref $self;
    
    # Validation rule
    $rule ||= $self->rule;
    
    # Shared rule
    my $shared_rule = $self->shared_rule;
    
    # Data filter
    my $filter = $self->data_filter;
    $data = $filter->($data) if $filter;
    
    # Check data
    croak "First argument must be hash ref"
      unless ref $data eq 'HASH';
    
    # Check rule
    croak "Validation rule must be array ref\n" .
          "(see syntax of validation rule 1)\n" .
          $self->_rule_syntax($rule)
      unless ref $rule eq 'ARRAY';
    
    # Result
    my $result = Validator::Custom::Result->new;
    $result->{_error_infos} = {};
    
    # Save raw data
    $result->raw_data($data);
    
    # Error is stock?
    my $error_stock = $self->error_stock;
    
    # Valid keys
    my $valid_keys = {};
    
    # Error position
    my $pos = 0;
    
    # Found missing paramteters
    my $found_missing_params = {};

    # Process each key
    OUTER_LOOP:
    for (my $i = 0; $i < @{$rule}; $i += 2) {
        
        # Increment position
        $pos++;
        
        # Key, options, and constraints
        my $key = $rule->[$i];
        my $opts = $rule->[$i + 1];
        my $constraints;
        if (ref $opts eq 'HASH') {
            $constraints = $rule->[$i + 2];
            $i++;
        }
        else {
            $constraints = $opts;
            $opts = {};
        }
        
        # Check constraints
        croak "Constraints of validation rule must be array ref\n" .
              "(see syntax of validation rule 2)\n" .
              $self->_rule_syntax($rule)
          unless ref $constraints eq 'ARRAY';
        
        # Arrange key
        my $result_key = $key;
        if (ref $key eq 'HASH') {
            my $first_key = (keys %$key)[0];
            $result_key = $first_key;
            $key         = $key->{$first_key};
        }
        
        # Real keys
        my $keys;
        
        if (ref $key eq 'ARRAY') {
            $keys = $key;
        }
        elsif (ref $key eq 'Regexp') {
           $keys = [];
           foreach my $k (keys %$data) {
               push @$keys, $k if $k =~ /$key/;
           }
        }
        else {
            $keys = [$key];
        }
        # Check option
        foreach my $oname (keys %$opts) {
            croak qq{Option "$oname" of "$result_key" is invalid name}
              unless $VALID_OPTIONS{$oname};
        }
        
        # Is data copy?
        my $copy = 1;
        $copy = $opts->{copy} if exists $opts->{copy};
        
        # Check missing parameters
        my $require = exists $opts->{require} ? $opts->{require} : 1;
        my $found_missing_param;
        my $missing_params = $result->missing_params;
        foreach my $key (@$keys) {
            unless (exists $data->{$key}) {
                if ($require) {
                    push @$missing_params, $key
                      unless $found_missing_params->{$key};
                    $found_missing_params->{$key}++;
                }
                $found_missing_param = 1;
            }
        }
        if ($found_missing_param) {
            $result->data->{$result_key} = ref $opts->{default} eq 'CODE'
                                         ? $opts->{default}->($self)
                                         : $opts->{default}
              if exists $opts->{default} && $copy;
            next;
        }
        
        # Already valid
        next if $valid_keys->{$result_key};
        
        # Add shared rule
        push @$constraints, @$shared_rule;
        
        # Validation
        my $value = @$keys > 1
                  ? [map { $data->{$_} } @$keys]
                  : $data->{$keys->[0]};

        foreach my $constraint (@$constraints) {
            
            # Arrange constraint information
            my ($constraint, $message)
              = ref $constraint eq 'ARRAY' ? @$constraint : ($constraint);
            
            # Data type
            my $data_type = {};
            
            # Arguments
            my $arg;
            
            # Arrange constraint
            if(ref $constraint eq 'HASH') {
                my $first_key = (keys %$constraint)[0];
                $arg        = $constraint->{$first_key};
                $constraint = $first_key;
            }
            
            # Constraint function
            my $cfuncs;
            my $negative;
            
            # Sub reference
            if( ref $constraint eq 'CODE') {
                
                # Constraint function
                $cfuncs = [$constraint];
            }
            
            # Constraint key
            else {
                
                # Constirnt infomation
                my $cinfo = $self->_parse_constraint($constraint);
                $data_type->{array} = 1 if $cinfo->{array};
                                                
                # Constraint function
                $cfuncs = $cinfo->{funcs};
            }
            
            # Is valid?
            my $is_valid;
            
            # Data is array
            if($data_type->{array}) {
                
                # To array
                $value = [$value] unless ref $value eq 'ARRAY';
                
                # Validation loop
                for (my $i = 0; $i < @$value; $i++) {
                    my $data = $value->[$i];
                    
                    # Validation
                    for (my $k = 0; $k < @$cfuncs; $k++) {
                        my $cfunc = $cfuncs->[$k];
                        
                        # Validate
                        my $cresult = $cfunc->($data, $arg, $self);
                        
                        # Constrint result
                        my $v;
                        if (ref $cresult eq 'ARRAY') {
                            ($is_valid, $v) = @$cresult;
                            $value->[$i] = $v;
                        }
                        else {
                            $is_valid = $cresult;
                        }
                        
                        last if $is_valid;
                    }
                    
                    # Validation error
                    last unless $is_valid;
                }
            }
            
            # Data is scalar
            else {

                # Validation
                foreach my $cfunc (@$cfuncs) {
                    my $cresult = $cfunc->($value, $arg, $self);
                    
                    if (ref $cresult eq 'ARRAY') {
                        my $v;
                        ($is_valid, $v) = @$cresult;
                        $value = $v if $is_valid;
                    }
                    else {
                        $is_valid = $cresult;
                    }
                    
                    last if $is_valid;
                }
            }
            
            # Add error if it is invalid
            unless ($is_valid) {
                
                # Resist error info
                $message = $opts->{message} unless defined $message;
                $result->{_error_infos}->{$result_key} = {
                    message      => $message,
                    position     => $pos,
                    reason       => $constraint,
                    original_key => $key
                }
                  unless exists $result->{_error_infos}->{$result_key};
                
                # Set default value
                $result->data->{$result_key} = ref $opts->{default} eq 'CODE'
                                             ? $opts->{default}->($self)
                                             : $opts->{default}
                  if exists $opts->{default} && $copy;
                
                # No Error strock
                unless ($error_stock) {
                    # Check rest constraint
                    my $found;
                    for (my $k = $i + 2; $k < @{$rule}; $k += 2) {
                        my $key = $rule->[$k];
                        $k++ if ref $rule->[$k + 1] eq 'HASH';
                        $key = (keys %$key)[0] if ref $key eq 'HASH';
                        $found = 1 if $key eq $result_key;
                    }
                    last OUTER_LOOP unless $found;
                }
                next OUTER_LOOP;
            }
        }
        
        # Result data
        $result->data->{$result_key} = $value if $copy;
        
        # Key is valid
        $valid_keys->{$result_key} = 1;
        
        # Remove invalid key
        delete $result->{_error_infos}->{$key};
    }
    
    return $result;
}

sub _parse_constraint {
    my ($self, $constraint) = @_;
    
    # Constraint infomation
    my $cinfo = {};
    
    # Simple constraint name
    unless ($constraint =~ /\W/) {
        my $cfunc = $self->constraints->{$constraint} || '';
        croak qq{"$constraint" is not registered}
          unless ref $cfunc eq 'CODE';
        
        $cinfo->{funcs} = [$cfunc];
        return $cinfo;
    }

    # Trim space
    $constraint ||= '';
    $constraint =~ s/^\s+//;
    $constraint =~ s/\s+$//;
    
    # Target is array elemetns
    $cinfo->{array} = 1 if $constraint =~ s/^@//;
    croak qq{"\@" must be one at the top of constrinat name}
      if index($constraint, '@') > -1;
    
    # Constraint functions
    my @cfuncs;
    
    # Constraint names
    my @cnames = split(/\|\|/, $constraint);
    
    # Convert constarint names to constraint funcions
    foreach my $cname (@cnames) {
        $cname ||= '';
        
        # Trim space
        $cname =~ s/^\s+//;
        $cname =~ s/\s+$//;
        
        # Negative
        my $negative = $cname =~ s/^!// ? 1 : 0;
        croak qq{"!" must be one at the top of constraint name}
          if index($cname, '!') > -1;
        
        # Trim space
        $cname =~ s/^\s+//;
        $cname =~ s/\s+$//;
        
        # Constraint function
        croak "Constraint name '$cname' must be [A-Za-z0-9_]"
          if $cname =~ /\W/;
        my $cfunc = $self->constraints->{$cname} || '';
        croak qq{"$cname" is not registered}
          unless ref $cfunc eq 'CODE';
        
        # Negativate
        my $f = $negative ? sub {
            my $ret = $cfunc->(@_);
            
            if (ref $ret eq 'ARRAY') {
                $ret->[0] = ! $ret->[0];
                return $ret;
            }
            else {
                return ! $ret;
            }
        } : $cfunc;
        
        # Add
        push @cfuncs, $f;
    }
    
    $cinfo->{funcs} = \@cfuncs;
    
    return $cinfo;
}

sub _parse_random_string_rule {
    my $self = shift;
    
    # Rule
    my $rule = ref $_[0] eq 'HASH' ? $_[0] : {@_};
    
    # Result
    my $result = {};
    
    # Parse string rule
    foreach my $name (keys %$rule) {
        
        # Pettern
        my $pattern = $rule->{$name};
        $pattern = '' unless $pattern;
        
        # State
        my $state = 'character';

        # Count
        my $count = '';
        
        # Chacacter sets
        my $csets = [];
        my $cset = [];
        
        # Parse pattern
        my $c;
        while (defined ($c = substr($pattern, 0, 1, '')) && length $c) {
            
            # Character class
            if ($state eq 'character_class') {
                if ($c eq ']') {
                    $state = 'character';
                    push @$csets, $cset;
                    $cset = [];
                    $state = 'character';
                }
                else {
                    push @$cset, $c;
                }
            }
            
            # Count
            elsif ($state eq 'count') {
                if ($c eq '}') {
                    $count = 1 if $count < 1;
                    for (my $i = 0; $i < $count - 1; $i++) {
                        push @$csets, [@{$csets->[-1] || ['']}];
                    }
                    $count = '';
                    $state = 'character';
                }
                else {
                    $count .= $c;
                }
            }
            
            # Character
            else {
                if ($c eq '[') {
                    $state = 'character_class';
                }
                elsif ($c eq '{') {
                    $state = 'count';
                }
                else {
                    push @$csets, [$c];
                }
            }
        }
        
        # Add Charcter sets
        $result->{$name} = $csets;
    }
    
    return $result;
}

sub _rule_syntax {
    my ($self, $rule) = @_;
    
    my $message = $self->syntax;
    
    require Data::Dumper;
    $message .= "### Your validation rule:\n";
    $message .= Data::Dumper->Dump([$rule], ['$rule']);
    $message .= "\n";
    return $message;
}

# DEPRECATED!
has shared_rule => sub { [] };
# DEPRECATED!
__PACKAGE__->dual_attr('constraints', default => sub { {} },
                                      inherit => 'hash_copy');
1;