]> Dogcows Code - chaz/p5-CGI-Ex/blobdiff - lib/CGI/Ex/Validate.pm
CGI::Ex 2.22
[chaz/p5-CGI-Ex] / lib / CGI / Ex / Validate.pm
index 591168f1bfbae72bc729ef9067da56a0ea47cc0b..37201955b59166afc36f67fd0f73b5556eed579c 100644 (file)
@@ -1,17 +1,18 @@
 package CGI::Ex::Validate;
 
-### CGI Extended Validator
+=head1 NAME
+
+CGI::Ex::Validate - The "Just Right" form validator with javascript in parallel
+
+=cut
 
 ###----------------------------------------------------------------###
-#  Copyright 2004 - Paul Seamons                                     #
+#  Copyright 2007 - Paul Seamons                                     #
 #  Distributed under the Perl Artistic License without warranty      #
 ###----------------------------------------------------------------###
 
-### See perldoc at bottom
-
 use strict;
 use vars qw($VERSION
-            $ERROR_PACKAGE
             $DEFAULT_EXT
             %DEFAULT_OPTIONS
             $JS_URI_PATH
@@ -21,25 +22,19 @@ use vars qw($VERSION
             @UNSUPPORTED_BROWSERS
             );
 
-$VERSION = '1.14';
+$VERSION = '2.22';
 
-$ERROR_PACKAGE = 'CGI::Ex::Validate::Error';
 $DEFAULT_EXT   = 'val';
 $QR_EXTRA      = qr/^(\w+_error|as_(array|string|hash)_\w+|no_\w+)/;
 @UNSUPPORTED_BROWSERS = (qr/MSIE\s+5.0\d/i);
 
-use CGI::Ex::Conf ();
-
 ###----------------------------------------------------------------###
 
 sub new {
-  my $class = shift || __PACKAGE__;
-  my $self  = (@_ && ref($_[0])) ? shift : {@_};
+  my $class = shift;
+  my $self  = ref($_[0]) ? shift : {@_};
 
-  ### allow for global defaults
-  foreach (keys %DEFAULT_OPTIONS) {
-    $self->{$_} = $DEFAULT_OPTIONS{$_} if ! exists $self->{$_};
-  }
+  $self = {%DEFAULT_OPTIONS, %$self} if scalar keys %DEFAULT_OPTIONS;
 
   return bless $self, $class;
 }
@@ -47,184 +42,151 @@ sub new {
 ###----------------------------------------------------------------###
 
 sub cgix {
-  my $self = shift;
-  return $self->{cgix} ||= do {
-    require CGI::Ex;
-    CGI::Ex->new;
-  };
-}
-
-sub conf {
-  my $self = shift;
-  return $self->{conf_obj} ||= CGI::Ex::Conf->new({
-    default_ext => $DEFAULT_EXT,
-    directive   => 'LAST',
-  });
+    my $self = shift;
+    return $self->{'cgix'} ||= do {
+        require CGI::Ex;
+        CGI::Ex->new;
+    };
 }
 
 ### the main validation routine
 sub validate {
-  my $self = (! ref($_[0])) ? shift->new                    # $class->validate
-              : UNIVERSAL::isa($_[0], __PACKAGE__) ? shift  # $self->validate
-              : __PACKAGE__->new;                           # &validate
-  my $form     = shift || die "Missing form hash";
-  my $val_hash = shift || die "Missing validation hash";
-  my $what_was_validated = shift; # allow for extra arrayref that stores what was validated
-
-  ### turn the form into a form if it is really a CGI object
-  if (! ref($form)) {
-    die "Invalid form hash or cgi object";
-  } elsif(! UNIVERSAL::isa($form,'HASH')) {
-    local $self->{cgi_object} = $form;
-    $form = $self->cgix->get_form($form);
-  }
+    my $self = (! ref($_[0])) ? shift->new                    # $class->validate
+                : UNIVERSAL::isa($_[0], __PACKAGE__) ? shift  # $self->validate
+                : __PACKAGE__->new;                           # &validate
+    my $form     = shift || die "Missing form hash";
+    my $val_hash = shift || die "Missing validation hash";
+    my $what_was_validated = shift; # allow for extra arrayref that stores what was validated
+
+    ### turn the form into a form hash if doesn't look like one already
+    die "Invalid form hash or cgi object" if ! ref $form;
+    if (ref $form ne 'HASH') {
+        local $self->{cgi_object} = $form;
+        $form = $self->cgix->get_form($form);
+    }
+
+    ### make sure the validation is a hashref
+    ### get_validation handle odd types
+    if (ref $val_hash ne 'HASH') {
+        $val_hash = $self->get_validation($val_hash) if ref $val_hash ne 'SCALAR' || ! ref $val_hash;
+        die "Validation groups must be a hashref"    if ref $val_hash ne 'HASH';
+    }
 
-  ### get the validation - let get_validation deal with types
-  ### if a ref is not passed - assume it is a filename
-  $val_hash = $self->get_validation($val_hash);
-
-  ### allow for validation passed as single group hash, single group array,
-  ### or array of group hashes or group arrays
-  my @ERRORS = ();
-  my %EXTRA  = ();
-  my @USED_GROUPS = ();
-  my $group_order = (UNIVERSAL::isa($val_hash,'HASH')) ? [$val_hash] : $val_hash;
-  foreach my $group_val (@$group_order) {
-    die "Validation groups must be a hashref" if ! UNIVERSAL::isa($group_val,'HASH');
-    my $title       = $group_val->{'group title'};
-    my $validate_if = $group_val->{'group validate_if'};
+    ### parse keys that are group arguments - and those that are keys to validate
+    my %ARGS;
+    my @field_keys = grep { /^(?:group|general)\s+(\w+)/
+                              ? do {$ARGS{$1} = $val_hash->{$_} ; 0}
+                              : 1 }
+                     sort keys %$val_hash;
 
     ### only validate this group if it is supposed to be checked
-    next if $validate_if && ! $self->check_conditional($form, $validate_if);
-    push @USED_GROUPS, $group_val;
-
-    ### If the validation items were not passed as an arrayref.
-    ### Look for a group order and then fail back to the keys of the group.
-    ### We will keep track of what was added using %found - the keys will
-    ###   be the hash signatures of the field_val hashes (ignore the hash internals).
-    my @order  = sort keys %$group_val;
-    my $fields = $group_val->{'group fields'};
-    my %found = (); # attempt to keep track of what field_vals have been added
-    if ($fields) { # if I passed group fields array - use it
-      die "'group fields' must be an arrayref" if ! UNIVERSAL::isa($fields,'ARRAY');
-    } else { # other wise - create our own array
-      my @fields = ();
-      if (my $order = $group_val->{'group order'} || \@order) {
-        die "Validation 'group order' must be an arrayref" if ! UNIVERSAL::isa($order,'ARRAY');
-        foreach my $field (@$order) {
-          next if $field =~ /^(group|general)\s/;
-          my $field_val = exists($group_val->{$field}) ? $group_val->{$field}
-            : ($field eq 'OR') ? 'OR' : die "No element found in group for $field";
-          $found{"$field_val"} = 1; # do this before modifying on the next line
-          if (ref $field_val && ! $field_val->{'field'}) {
-            $field_val = { %$field_val, 'field' => $field }; # copy the values to add the key
-          }
-          push @fields, $field_val;
+    return if $ARGS{'validate_if'} && ! $self->check_conditional($form, $ARGS{'validate_if'});
+
+    ### Look first for items in 'group fields' or 'group order'
+    my $fields;
+    if ($fields = $ARGS{'fields'} || $ARGS{'order'}) {
+        my $type = $ARGS{'fields'} ? 'group fields' : 'group order';
+        die "Validation '$type' must be an arrayref when passed"
+            if ! UNIVERSAL::isa($fields, 'ARRAY');
+        my @temp;
+        foreach my $field (@$fields) {
+            die "Non-defined value in '$type'" if ! defined $field;
+            if (ref $field) {
+                die "Found nonhashref value in '$type'" if ref($field) ne 'HASH';
+                die "Element missing \"field\" key/value in '$type'" if ! defined $field->{'field'};
+                push @temp, $field;
+            } elsif ($field eq 'OR') {
+                push @temp, 'OR';
+            } else {
+                die "No element found in '$type' for $field" if ! exists $val_hash->{$field};
+                die "Found nonhashref value in '$type'" if ref($val_hash->{$field}) ne 'HASH';
+                push @temp, { %{ $val_hash->{$field} }, field => $field }; # copy the values to add the key
+            }
         }
-      }
-      $fields = \@fields;
-    }
+        $fields = \@temp;
 
-    ### double check which field_vals have been used so far
-    foreach my $field_val (@$fields) {
-      my $field = $field_val->{'field'} || die "Missing field key in validation";
-      $found{"$field_val"} = 1;
+        ### limit the keys that need to be searched to those not in fields or order
+        my %found = map { $_->{'field'} => 1 } @temp;
+        @field_keys = grep { ! $found{$_} } @field_keys;
     }
 
-    ### add any remaining field_vals from the order
+    ### add any remaining field_vals from our original hash
     ### this is necessary for items that weren't in group fields or group order
-    foreach my $field (@order) {
-      next if $field =~ /^(group|general)\s/;
-      my $field_val = $group_val->{$field};
-      die "Found a nonhashref value on field $field" if ! UNIVERSAL::isa($field_val, 'HASH');
-      next if $found{"$field_val"}; # do before modifying ref on next line
-      $field_val = { %$field_val, 'field' => $field } if ! $field_val->{'field'}; # copy the values
-      push @$fields, $field_val;
+    foreach my $field (@field_keys) {
+        die "Found nonhashref value for field $field" if ref($val_hash->{$field}) ne 'HASH';
+        if (defined $val_hash->{$field}->{'field'}) {
+            push @$fields, $val_hash->{$field};
+        } else {
+            push @$fields, { %{$val_hash->{$field}}, field => $field };
+        }
     }
+    return if ! $fields;
 
     ### Finally we have our arrayref of hashrefs that each have their 'field' key
     ### now lets do the validation
     my $found  = 1;
-    my @errors = ();
-    my $hold_error; # hold the error for a moment - to allow for an "Or" operation
-    foreach (my $i = 0; $i <= $#$fields; $i ++) {
-      my $ref = $fields->[$i];
-      if (! ref($ref) && $ref eq 'OR') {
-        $i ++ if $found; # if found skip the OR altogether
-        $found = 1; # reset
-        next;
-      }
-      $found = 1;
-      die "Missing field key during normal validation" if ! $ref->{'field'};
-      local $ref->{'was_validated'} = 1;
-      my @err = $self->validate_buddy($form, $ref->{'field'}, $ref);
-      if (delete($ref->{'was_validated'}) && $what_was_validated) {
-        push @$what_was_validated, $ref;
-      }
+    my @errors;
+    my $hold_error; # hold the error for a moment - to allow for an "OR" operation
+    foreach (my $i = 0; $i < @$fields; $i++) {
+        my $ref = $fields->[$i];
+        if (! ref($ref) && $ref eq 'OR') {
+            $i++ if $found; # if found skip the OR altogether
+            $found = 1; # reset
+            next;
+        }
+        $found = 1;
+        die "Missing field key during normal validation" if ! $ref->{'field'};
+        local $ref->{'was_validated'} = 1;
+        my $err = $self->validate_buddy($form, $ref->{'field'}, $ref);
+        if ($ref->{'was_validated'} && $what_was_validated) {
+            push @$what_was_validated, $ref;
+        }
 
-      ### test the error - if errors occur allow for OR - if OR fails use errors from first fail
-      if (scalar @err) {
-        if ($i < $#$fields && ! ref($fields->[$i + 1]) && $fields->[$i + 1] eq 'OR') {
-          $hold_error = \@err;
+        ### test the error - if errors occur allow for OR - if OR fails use errors from first fail
+        if ($err) {
+            if ($i < $#$fields && ! ref($fields->[$i + 1]) && $fields->[$i + 1] eq 'OR') {
+                $hold_error = $err;
+            } else {
+                push @errors, $hold_error ? @$hold_error : @$err;
+                $hold_error = undef;
+            }
         } else {
-          push @errors, $hold_error ? @$hold_error : @err;
-          $hold_error = undef;
+            $hold_error = undef;
         }
-      } else {
-        $hold_error = undef;
-      }
     }
     push(@errors, @$hold_error) if $hold_error; # allow for final OR to work
 
-    ### add on errors as requested
-    if ($#errors != -1) {
-      push @ERRORS, $title if $title;
-      push @ERRORS, @errors;
-    }
 
-    ### add on general options, and group options if errors in group occurred
-    foreach my $field (@order) {
-      next if $field !~ /^(general|group)\s+(\w+)$/;
-      my $key = $2;
-      next if $1 eq 'group' && ($#errors == -1 || $key =~ /^(field|order|title)$/);
-      $EXTRA{$key} = $group_val->{$field};
+    ### optionally check for unused keys in the form
+    if ($ARGS{no_extra_fields} || $self->{no_extra_fields}) {
+        my %keys = map { ($_->{'field'} => 1) } @$fields; # %{ $self->get_validation_keys($val_hash) };
+        foreach my $key (sort keys %$form) {
+            next if $keys{$key};
+            push @errors, [$key, 'no_extra_fields', {}, undef];
+        }
     }
-  }
-
-  ### store any extra items from self
-  foreach my $key (keys %$self) {
-    next if $key !~ $QR_EXTRA;
-    $EXTRA{$key} = $self->{$key};
-  }
 
-  ### allow for checking for unused keys
-  if ($EXTRA{no_extra_fields}) {
-    my $which = ($EXTRA{no_extra_fields} =~ /used/i) ? 'used' : 'all';
-    my $ref   = ($which eq 'all') ? $val_hash : \@USED_GROUPS;
-    my $keys  = $self->get_validation_keys($ref);
-    foreach my $key (sort keys %$form) {
-      next if $keys->{$key};
-      $self->add_error(\@ERRORS, $key, 'no_extra_fields', {}, undef);
+    ### return what they want
+    if (@errors) {
+        my @copy = grep {/$QR_EXTRA/o} keys %$self;
+        @ARGS{@copy} = @{ $self }{@copy};
+        unshift @errors, $ARGS{'title'} if $ARGS{'title'};
+        my $err_obj = $self->new_error(\@errors, \%ARGS);
+        die    $err_obj if $ARGS{'raise_error'};
+        return $err_obj;
+    } else {
+        return;
     }
-  }
-
-  ### return what they want
-  if ($#ERRORS != -1) {
-    my $err_obj = $ERROR_PACKAGE->new(\@ERRORS, \%EXTRA);
-    die    $err_obj if $EXTRA{raise_error};
-    return $err_obj;
-  } else {
-    return wantarray ? () : undef;
-  }
 }
 
+sub new_error {
+    my $self = shift;
+    return CGI::Ex::Validate::Error->new(@_);
+}
 
 ### allow for optional validation on groups and on individual items
 sub check_conditional {
-  my ($self, $form, $ifs, $N_level, $ifs_match) = @_;
-
-  $N_level ||= 0;
-  $N_level ++; # prevent too many recursive checks
+  my ($self, $form, $ifs, $ifs_match) = @_;
 
   ### can pass a single hash - or an array ref of hashes
   if (! $ifs) {
@@ -235,6 +197,8 @@ sub check_conditional {
     $ifs = [$ifs];
   }
 
+  local $self->{'_check_conditional'} = 1;
+
   ### run the if options here
   ### multiple items can be passed - all are required unless OR is used to separate
   my $found = 1;
@@ -259,8 +223,8 @@ sub check_conditional {
     my $field = $ref->{'field'} || die "Missing field key during validate_if (possibly used a reference to a main hash *foo -> &foo)";
     $field =~ s/\$(\d+)/defined($ifs_match->[$1]) ? $ifs_match->[$1] : ''/eg if $ifs_match;
 
-    my @err = $self->validate_buddy($form, $field, $ref, $N_level);
-    $found = 0 if scalar @err;
+    my $errs = $self->validate_buddy($form, $field, $ref);
+    $found = 0 if $errs;
   }
   return $found;
 }
@@ -269,18 +233,18 @@ sub check_conditional {
 ### this is where the main checking goes on
 sub validate_buddy {
   my $self = shift;
-  my ($form, $field, $field_val, $N_level, $ifs_match) = @_;
-  $N_level ||= 0;
-  $N_level ++; # prevent too many recursive checks
-  die "Max dependency level reached $N_level" if $N_level > 10;
+  my ($form, $field, $field_val, $ifs_match) = @_;
+
+  local $self->{'_recurse'} = ($self->{'_recurse'} || 0) + 1;
+  die "Max dependency level reached 10" if $self->{'_recurse'} > 10;
 
   my @errors = ();
   my $types  = [sort keys %$field_val];
 
   ### allow for not running some tests in the cgi
-  if (scalar $self->filter_type('exclude_cgi',$types)) {
+  if ($field_val->{'exclude_cgi'}) {
     delete $field_val->{'was_validated'};
-    return wantarray ? @errors : $#errors + 1;
+    return 0;
   }
 
   ### allow for field names that contain regular expressions
@@ -290,55 +254,61 @@ sub validate_buddy {
     die "The e option cannot be used on validation keys on field $field" if $opt =~ /e/;
     foreach my $_field (sort keys %$form) {
       next if ($not && $_field =~ m/(?$opt:$pat)/) || (! $not && $_field !~ m/(?$opt:$pat)/);
-      my @match = (undef,$1,$2,$3,$4,$5); # limit to the matches
-      push @errors, $self->validate_buddy($form, $_field, $field_val, $N_level, \@match);
+      my @match = (undef, $1, $2, $3, $4, $5); # limit to the matches
+      my $errs = $self->validate_buddy($form, $_field, $field_val, \@match);
+      push @errors, @$errs if $errs;
     }
-    return wantarray ? @errors : $#errors + 1;
+    return @errors ? \@errors : 0;
   }
 
+  my $values   = UNIVERSAL::isa($form->{$field},'ARRAY') ? $form->{$field} : [$form->{$field}];
+  my $n_values = $#$values + 1;
+
   ### allow for default value
-  foreach my $type ($self->filter_type('default', $types)) {
-    if (! defined($form->{$field}) || (! ref($form->{$field}) && ! length($form->{$field}))) {
-      $form->{$field} = $field_val->{$type};
+  if (exists $field_val->{'default'}) {
+    if ($n_values == 0 || ($n_values == 1 && (! defined($values->[0]) || ! length($values->[0])))) {
+      $form->{$field} = $values->[0] = $field_val->{'default'};
     }
   }
 
-  my $n_values = UNIVERSAL::isa($form->{$field},'ARRAY') ? $#{ $form->{$field} } + 1 : 1;
-  my $values = ($n_values > 1) ? $form->{$field} : [$form->{$field}];
-
   ### allow for a few form modifiers
   my $modified = 0;
   foreach my $value (@$values) {
     next if ! defined $value;
-    if (! scalar $self->filter_type('do_not_trim',$types)) { # whitespace
+    if (! $field_val->{'do_not_trim'}) { # whitespace
       $value =~ s/^\s+//;
       $value =~ s/\s+$//;
       $modified = 1;
     }
-    if (scalar $self->filter_type('to_upper_case',$types)) { # uppercase
+    if ($field_val->{'trim_control_chars'}) {
+      $value =~ y/\t/ /;
+      $value =~ y/\x00-\x1F//d;
+      $modified = 1;
+    }
+    if ($field_val->{'to_upper_case'}) { # uppercase
       $value = uc($value);
       $modified = 1;
-    } elsif (scalar $self->filter_type('to_lower_case',$types)) { # lowercase
+    } elsif ($field_val->{'to_lower_case'}) { # lowercase
       $value = lc($value);
       $modified = 1;
     }
   }
   # allow for inline specified modifications (ie s/foo/bar/)
-  foreach my $type ($self->filter_type('replace',$types)) {
+  foreach my $type (grep {/^replace_?\d*$/} @$types) {
     my $ref = UNIVERSAL::isa($field_val->{$type},'ARRAY') ? $field_val->{$type}
       : [split(/\s*\|\|\s*/,$field_val->{$type})];
     foreach my $rx (@$ref) {
       if ($rx !~ m/^\s*s([^\s\w])(.+)\1(.*)\1([eigsmx]*)$/s) {
-        die "Not sure how to parse that match ($rx)";
+        die "Not sure how to parse that replace ($rx)";
       }
-      my ($pat,$swap,$opt) = ($2,$3,$4);
+      my ($pat, $swap, $opt) = ($2, $3, $4);
       die "The e option cannot be used in swap on field $field" if $opt =~ /e/;
       my $global = $opt =~ s/g//g;
       $swap =~ s/\\n/\n/g;
       if ($global) {
         foreach my $value (@$values) {
           $value =~ s{(?$opt:$pat)}{
-            my @match = (undef,$1,$2,$3,$4,$5,$6); # limit on the number of matches
+            my @match = (undef, $1, $2, $3, $4, $5, $6); # limit on the number of matches
             my $copy = $swap;
             $copy =~ s/\$(\d+)/defined($match[$1]) ? $match[$1] : ""/ge;
             $modified = 1;
@@ -347,8 +317,9 @@ sub validate_buddy {
         }
       }else{
         foreach my $value (@$values) {
+          next if ! defined $value;
           $value =~ s{(?$opt:$pat)}{
-            my @match = (undef,$1,$2,$3,$4,$5,$6); # limit on the number of matches
+            my @match = (undef, $1, $2, $3, $4, $5, $6); # limit on the number of matches
             my $copy = $swap;
             $copy =~ s/\$(\d+)/defined($match[$1]) ? $match[$1] : ""/ge;
             $modified = 1;
@@ -374,70 +345,56 @@ sub validate_buddy {
   ### only continue if a validate_if is not present or passes test
   my $needs_val = 0;
   my $n_vif = 0;
-  foreach my $type ($self->filter_type('validate_if',$types)) {
+  foreach my $type (grep {/^validate_if_?\d*$/} @$types) {
     $n_vif ++;
     my $ifs = $field_val->{$type};
-    my $ret = $self->check_conditional($form, $ifs, $N_level, $ifs_match);
+    my $ret = $self->check_conditional($form, $ifs, $ifs_match);
     $needs_val ++ if $ret;
   }
   if (! $needs_val && $n_vif) {
     delete $field_val->{'was_validated'};
-    return wantarray ? @errors : $#errors + 1;
+    return 0;
   }
 
   ### check for simple existence
   ### optionally check only if another condition is met
-  my $is_required = '';
-  foreach my $type ($self->filter_type('required',$types)) {
-    next if ! $field_val->{$type};
-    $is_required = $type;
-    last;
-  }
+  my $is_required = $field_val->{'required'} ? 'required' : '';
   if (! $is_required) {
-    foreach my $type ($self->filter_type('required_if',$types)) {
+    foreach my $type (grep {/^required_if_?\d*$/} @$types) {
       my $ifs = $field_val->{$type};
-      next if ! $self->check_conditional($form, $ifs, $N_level, $ifs_match);
+      next if ! $self->check_conditional($form, $ifs, $ifs_match);
       $is_required = $type;
       last;
     }
   }
-  if ($is_required && (! defined($form->{$field})
-                       || ((UNIVERSAL::isa($form->{$field},'ARRAY') && $#{ $form->{$field} } == -1)
-                           || ! length($form->{$field})))) {
-    return 1 if ! wantarray;
-    $self->add_error(\@errors, $field, $is_required, $field_val, $ifs_match);
-    return @errors;
+  if ($is_required
+      && ($n_values == 0 || ($n_values == 1 && (! defined($values->[0]) || ! length $values->[0])))) {
+    return [] if $self->{'_check_conditional'};
+    return [[$field, $is_required, $field_val, $ifs_match]];
   }
 
   ### min values check
-  foreach my $type ($self->filter_type('min_values',$types)) {
-    my $n = $field_val->{$type} || 0;
-    if ($n_values < $n) {
-      return 1 if ! wantarray;
-      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
-      return @errors;
-    }
+  my $n = exists($field_val->{'min_values'}) ? $field_val->{'min_values'} || 0 : 0;
+  if ($n_values < $n) {
+    return [] if $self->{'_check_conditional'};
+    return [[$field, 'min_values', $field_val, $ifs_match]];
   }
 
   ### max values check
-  my @keys = $self->filter_type('max_values',$types);
-  if ($#keys == -1) {
-    push @keys, 'max_values';
-    $field_val->{'max_values'} = 1;
-  }
-  foreach my $type (@keys) {
-    my $n = $field_val->{$type} || 0;
-    if ($n_values > $n) {
-      return 1 if ! wantarray;
-      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
-      return @errors;
-    }
+  $field_val->{'max_values'} = 1 if ! exists $field_val->{'max_values'};
+  $n = $field_val->{'max_values'} || 0;
+  if ($n_values > $n) {
+    return [] if $self->{'_check_conditional'};
+    return [[$field, 'max_values', $field_val, $ifs_match]];
   }
 
   ### max_in_set and min_in_set checks
-  foreach my $minmax (qw(min max)) {
-    my @keys = $self->filter_type("${minmax}_in_set",$types);
-    foreach my $type (@keys) {
+  my @min = grep {/^min_in_set_?\d*$/} @$types;
+  my @max = grep {/^max_in_set_?\d*$/} @$types;
+  foreach ([min => \@min],
+           [max => \@max]) {
+    my ($minmax, $keys) = @$_;
+    foreach my $type (@$keys) {
       $field_val->{$type} =~ m/^\s*(\d+)(?i:\s*of)?\s+(.+)\s*$/
         || die "Invalid in_set check $field_val->{$type}";
       my $n = $1;
@@ -449,9 +406,8 @@ sub validate_buddy {
       }
       if (   ($minmax eq 'min' && $n > 0)
           || ($minmax eq 'max' && $n < 0)) {
-        return 1 if ! wantarray;
-        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
-        return @errors;
+        return [] if $self->{'_check_conditional'};
+        return [[$field, $type, $field_val, $ifs_match]];
       }
     }
   }
@@ -463,21 +419,21 @@ sub validate_buddy {
   foreach my $value (@$values) {
 
     ### allow for enum types
-    foreach my $type ($self->filter_type('enum',$types)) {
-      my $ref = ref($field_val->{$type}) ? $field_val->{$type} : [split(/\s*\|\|\s*/,$field_val->{$type})];
+    if (exists $field_val->{'enum'}) {
+      my $ref = ref($field_val->{'enum'}) ? $field_val->{'enum'} : [split(/\s*\|\|\s*/,$field_val->{'enum'})];
       my $found = 0;
       foreach (@$ref) {
         $found = 1 if defined($value) && $_ eq $value;
       }
       if (! $found) {
-        return 1 if ! wantarray;
-        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+        return [] if $self->{'_check_conditional'};
+        push @errors, [$field, 'enum', $field_val, $ifs_match];
       }
       $content_checked = 1;
     }
 
     ### field equality test
-    foreach my $type ($self->filter_type('equals',$types)) {
+    foreach my $type (grep {/^equals_?\d*$/} @$types) {
       my $field2  = $field_val->{$type};
       my $not     = ($field2 =~ s/^!\s*//) ? 1 : 0;
       my $success = 0;
@@ -490,39 +446,39 @@ sub validate_buddy {
         $success = 1; # occurs if they are both undefined
       }
       if ($not ? $success : ! $success) {
-        return 1 if ! wantarray;
-        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+        return [] if $self->{'_check_conditional'};
+        push @errors, [$field, $type, $field_val, $ifs_match];
       }
       $content_checked = 1;
     }
 
     ### length min check
-    foreach my $type ($self->filter_type('min_len',$types)) {
-      my $n = $field_val->{$type};
+    if (exists $field_val->{'min_len'}) {
+      my $n = $field_val->{'min_len'};
       if (! defined($value) || length($value) < $n) {
-        return 1 if ! wantarray;
-        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+        return [] if $self->{'_check_conditional'};
+        push @errors, [$field, 'min_len', $field_val, $ifs_match];
       }
     }
 
     ### length max check
-    foreach my $type ($self->filter_type('max_len',$types)) {
-      my $n = $field_val->{$type};
+    if (exists $field_val->{'max_len'}) {
+      my $n = $field_val->{'max_len'};
       if (defined($value) && length($value) > $n) {
-        return 1 if ! wantarray;
-        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+        return [] if $self->{'_check_conditional'};
+        push @errors, [$field, 'max_len', $field_val, $ifs_match];
       }
     }
 
     ### now do match types
-    foreach my $type ($self->filter_type('match',$types)) {
+    foreach my $type (grep {/^match_?\d*$/} @$types) {
       my $ref = UNIVERSAL::isa($field_val->{$type},'ARRAY') ? $field_val->{$type}
          : UNIVERSAL::isa($field_val->{$type}, 'Regexp') ? [$field_val->{$type}]
          : [split(/\s*\|\|\s*/,$field_val->{$type})];
       foreach my $rx (@$ref) {
         if (UNIVERSAL::isa($rx,'Regexp')) {
           if (! defined($value) || $value !~ $rx) {
-            $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+              push @errors, [$field, $type, $field_val, $ifs_match];
           }
         } else {
           if ($rx !~ m/^(!\s*|)m([^\s\w])(.*)\2([eigsmx]*)$/s) {
@@ -534,8 +490,8 @@ sub validate_buddy {
           if ( (     $not && (  defined($value) && $value =~ m/(?$opt:$pat)/))
                || (! $not && (! defined($value) || $value !~ m/(?$opt:$pat)/))
                ) {
-            return 1 if ! wantarray;
-            $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+            return [] if $self->{'_check_conditional'};
+            push @errors, [$field, $type, $field_val, $ifs_match];
           }
         }
       }
@@ -543,7 +499,7 @@ sub validate_buddy {
     }
 
     ### allow for comparison checks
-    foreach my $type ($self->filter_type('compare',$types)) {
+    foreach my $type (grep {/^compare_?\d*$/} @$types) {
       my $ref = UNIVERSAL::isa($field_val->{$type},'ARRAY') ? $field_val->{$type}
         : [split(/\s*\|\|\s*/,$field_val->{$type})];
       foreach my $comp (@$ref) {
@@ -574,15 +530,15 @@ sub validate_buddy {
           die "Not sure how to compare \"$comp\"";
         }
         if (! $test) {
-          return 1 if ! wantarray;
-          $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+          return [] if $self->{'_check_conditional'};
+          push @errors, [$field, $type, $field_val, $ifs_match];
         }
       }
       $content_checked = 1;
     }
 
     ### server side sql type
-    foreach my $type ($self->filter_type('sql',$types)) {
+    foreach my $type (grep {/^sql_?\d*$/} @$types) {
       my $db_type = $field_val->{"${type}_db_type"};
       my $dbh = ($db_type) ? $self->{dbhs}->{$db_type} : $self->{dbh};
       if (! $dbh) {
@@ -596,26 +552,26 @@ sub validate_buddy {
       $field_val->{"${type}_error_if"} = 1 if ! defined $field_val->{"${type}_error_if"};
       if ( (! $return && $field_val->{"${type}_error_if"})
            || ($return && ! $field_val->{"${type}_error_if"}) ) {
-        return 1 if ! wantarray;
-        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+        return [] if $self->{'_check_conditional'};
+        push @errors, [$field, $type, $field_val, $ifs_match];
       }
       $content_checked = 1;
     }
 
     ### server side custom type
-    foreach my $type ($self->filter_type('custom',$types)) {
+    foreach my $type (grep {/^custom_?\d*$/} @$types) {
       my $check = $field_val->{$type};
       next if UNIVERSAL::isa($check, 'CODE') ? &$check($field, $value, $field_val, $type) : $check;
-      return 1 if ! wantarray;
-      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+      return [] if $self->{'_check_conditional'};
+      push @errors, [$field, $type, $field_val, $ifs_match];
       $content_checked = 1;
     }
 
     ### do specific type checks
-    foreach my $type ($self->filter_type('type',$types)) {
+    foreach my $type (grep {/^type_?\d*$/} @$types) {
       if (! $self->check_type($value,$field_val->{'type'},$field,$form)){
-        return 1 if ! wantarray;
-        $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+        return [] if $self->{'_check_conditional'};
+        push @errors, [$field, $type, $field_val, $ifs_match];
       }
       $content_checked = 1;
     }
@@ -623,10 +579,9 @@ sub validate_buddy {
 
   ### allow for the data to be "untainted"
   ### this is only allowable if the user ran some other check for the datatype
-  foreach my $type ($self->filter_type('untaint',$types)) {
-    last if $#errors != -1;
+  if ($field_val->{'untaint'} && $#errors == -1) {
     if (! $content_checked) {
-      $self->add_error(\@errors, $field, $type, $field_val, $ifs_match);
+        push @errors, [$field, 'untaint', $field_val, $ifs_match];
     } else {
       ### generic untainter - assuming the other required content_checks did good validation
       $_ = /(.*)/ ? $1 : die "Couldn't match?" foreach @$values;
@@ -643,27 +598,7 @@ sub validate_buddy {
   }
 
   ### all done - time to return
-  return wantarray ? @errors : $#errors + 1;
-}
-
-### simple error adder abstraction
-sub add_error {
-  my $self = shift;
-  my $errors = shift;
-  push @$errors, \@_;
-}
-
-### allow for multiple validations in the same hash
-### ie Match, Match1, Match2, Match234
-sub filter_type {
-  my $self  = shift;
-  my $type  = shift;
-  my $order = shift || die "Missing order array";
-  my @array = ();
-  foreach (@$order) {
-    push @array, $_ if /^\Q$type\E_?\d*$/;
-  }
-  return wantarray ? @array : $#array + 1;
+  return @errors ? \@errors : 0;
 }
 
 ###----------------------------------------------------------------###
@@ -687,7 +622,7 @@ sub check_type {
   ### the "username" portion of an email address
   } elsif ($type eq 'LOCAL_PART') {
     return 0 if ! defined($value) || ! length($value);
-    return 0 if $value =~ m/[^a-z0-9.\-\!\&]/;
+    return 0 if $value =~ m/[^a-z0-9.\-!&+]/;
     return 0 if $value =~ m/^[\.\-]/;
     return 0 if $value =~ m/[\.\-\&]$/;
     return 0 if $value =~ m/(\.\-|\-\.|\.\.)/;
@@ -753,9 +688,10 @@ sub check_type {
 ###----------------------------------------------------------------###
 
 sub get_validation {
-  my $self = shift;
-  my $val  = shift;
-  return $self->conf->read($val, {html_key => 'validation'});
+    my $self = shift;
+    my $val  = shift;
+    require CGI::Ex::Conf;
+    return CGI::Ex::Conf::conf_read($val, {html_key => 'validation', default_ext => $DEFAULT_EXT});
 }
 
 ### returns all keys from all groups - even if group has validate_if
@@ -763,55 +699,64 @@ sub get_validation_keys {
   my $self     = shift;
   my $val_hash = shift;
   my $form     = shift; # with optional form - will only return keys in validated groups
-  my %keys     = ();
 
-  ### if a form was passed - make sure it is a hashref
+  ### turn the form into a form hash if doesn't look like one already
   if ($form) {
-    if (! ref($form)) {
-      die "Invalid form hash or cgi object";
-    } elsif(! UNIVERSAL::isa($form,'HASH')) {
-      require CGI::Ex;
-      $form = CGI::Ex->new->get_form($form);
-    }
+      die "Invalid form hash or cgi object" if ! ref $form;
+      if (ref $form ne 'HASH') {
+          local $self->{cgi_object} = $form;
+          $form = $self->cgix->get_form($form);
+      }
   }
 
-  my $refs     = $self->get_validation($val_hash);
-  $refs = [$refs] if ! UNIVERSAL::isa($refs,'ARRAY');
-  foreach my $group_val (@$refs) {
-    die "Group found that was not a hashref" if ! UNIVERSAL::isa($group_val, 'HASH');
+  ### make sure the validation is a hashref
+  ### get_validation handle odd types
+  if (ref $val_hash ne 'HASH') {
+    $val_hash = $self->get_validation($val_hash) if ref $val_hash ne 'SCALAR' || ! ref $val_hash;
+    die "Validation groups must be a hashref"    if ref $val_hash ne 'HASH';
+  }
 
-    ### if form is passed, check to see if the group passed validation
-    if ($form) {
-      my $validate_if = $group_val->{'group validate_if'};
-      next if $validate_if && ! $self->check_conditional($form, $validate_if);
+  ### parse keys that are group arguments - and those that are keys to validate
+  my %ARGS;
+  my @field_keys = grep { /^(?:group|general)\s+(\w+)/
+                            ? do {$ARGS{$1} = $val_hash->{$_} ; 0}
+                            : 1 }
+                   sort keys %$val_hash;
+
+  ### only validate this group if it is supposed to be checked
+  return if $form && $ARGS{'validate_if'} && ! $self->check_conditional($form, $ARGS{'validate_if'});
+
+  ### Look first for items in 'group fields' or 'group order'
+  my %keys;
+  if (my $fields = $ARGS{'fields'} || $ARGS{'order'}) {
+    my $type = $ARGS{'fields'} ? 'group fields' : 'group order';
+    die "Validation '$type' must be an arrayref when passed"
+      if ! UNIVERSAL::isa($fields, 'ARRAY');
+    foreach my $field (@$fields) {
+        die "Non-defined value in '$type'" if ! defined $field;
+        if (ref $field) {
+            die "Found nonhashref value in '$type'" if ref($field) ne 'HASH';
+            die "Element missing \"field\" key/value in '$type'" if ! defined $field->{'field'};
+            $keys{$field->{'field'}} = $field->{'name'} || 1;
+        } elsif ($field eq 'OR') {
+        } else {
+            die "No element found in '$type' for $field" if ! exists $val_hash->{$field};
+            die "Found nonhashref value in '$type'" if ref($val_hash->{$field}) ne 'HASH';
+            $keys{$field} = $val_hash->{$field}->{'name'} || 1;
+        }
     }
+  }
 
-    if ($group_val->{"group fields"}) {
-      die "Group fields must be an arrayref" if ! UNIVERSAL::isa($group_val->{"group fields"}, 'ARRAY');
-      foreach my $field_val (@{ $group_val->{"group fields"} }) {
-        next if ! ref($field_val) && $field_val eq 'OR';
-        die "Field_val must be a hashref" if ! UNIVERSAL::isa($field_val, 'HASH');
-        my $key = $field_val->{'field'} || die "Missing field key in field_val hashref";
-        $keys{$key} = $field_val->{'name'} || 1;
-      }
-    } elsif ($group_val->{"group order"}) {
-      die "Group order must be an arrayref" if ! UNIVERSAL::isa($group_val->{"group order"}, 'ARRAY');
-      foreach my $key (@{ $group_val->{"group order"} }) {
-        my $field_val = $group_val->{$key};
-        next if ! $field_val && $key eq 'OR';
-        die "Field_val for $key must be a hashref" if ! UNIVERSAL::isa($field_val, 'HASH');
-        $key = $field_val->{'field'} if $field_val->{'field'};
-        $keys{$key} = $field_val->{'name'} || 1;
+  ### add any remaining field_vals from our original hash
+  ### this is necessary for items that weren't in group fields or group order
+  foreach my $field (@field_keys) {
+      next if $keys{$field};
+      die "Found nonhashref value for field $field" if ref($val_hash->{$field}) ne 'HASH';
+      if (defined $val_hash->{$field}->{'field'}) {
+          $keys{$val_hash->{$field}->{'field'}} = $val_hash->{$field}->{'name'} || 1;
+      } else {
+          $keys{$field} = $val_hash->{$field}->{'name'} || 1;
       }
-    }
-
-    ### get all others
-    foreach my $key (keys %$group_val) {
-      next if $key =~ /^(general|group)\s/;
-      my $field_val = $group_val->{$key};
-      next if ! UNIVERSAL::isa($field_val, 'HASH');
-      $keys{$key} = $field_val->{'name'} || 1;
-    }
   }
 
   return \%keys;
@@ -821,52 +766,68 @@ sub get_validation_keys {
 
 ### spit out a chunk that will do the validation
 sub generate_js {
-  ### allow for some browsers to not receive the validation
-  if ($ENV{HTTP_USER_AGENT}) {
-    foreach (@UNSUPPORTED_BROWSERS) {
-      next if $ENV{HTTP_USER_AGENT} !~ $_;
-      return "<!-- JS Validation not supported in this browser $_ -->"
-    }
-  }
+    ### allow for some browsers to not receive the validation js
+    return "<!-- JS validation not supported in this browser $_ -->"
+        if $ENV{'HTTP_USER_AGENT'} && grep {$ENV{'HTTP_USER_AGENT'} =~ $_} @UNSUPPORTED_BROWSERS;
+
+    my $self        = shift;
+    my $val_hash    = shift || die "Missing validation";
+    my $form_name   = shift || die "Missing form name";
+    my $js_uri_path = shift || $JS_URI_PATH;
+    $val_hash = $self->get_validation($val_hash);
+
+    ### store any extra items from self
+    my %EXTRA = ();
+    $EXTRA{"general $_"} = $self->{$_} for grep {/$QR_EXTRA/o} keys %$self; # add 'general' to be used in javascript
+
+    my $js_uri_path_validate = $JS_URI_PATH_VALIDATE || do {
+        die "Missing \$js_uri_path" if ! $js_uri_path;
+        "$js_uri_path/CGI/Ex/validate.js";
+    };
+
+    if (! $self->{'no_jsondump'} && eval { require CGI::Ex::JSONDump }) {
+        my $json = CGI::Ex::JSONDump->new({pretty => 1})->dump($val_hash);
+        return qq{<script src="$js_uri_path_validate"></script>
+<script>
+document.validation = $json;
+if (document.check_form) document.check_form("$form_name");
+</script>
+};
 
-  my $self        = shift;
-  my $val_hash    = shift || die "Missing validation";
-  my $form_name   = shift || die "Missing form name";
-  my $js_uri_path = shift || $JS_URI_PATH;
-  $val_hash = $self->get_validation($val_hash);
-  require YAML;
-
-  ### store any extra items from self
-  my %EXTRA = ();
-  foreach my $key (keys %$self) {
-    next if $key !~ $QR_EXTRA;
-    $EXTRA{"general $key"} = $self->{$key};
-  }
+    } elsif (! $self->{'no_json'} && eval { require JSON }) {
+        my $json = JSON->new(pretty => 1)->objToJson($val_hash);
+
+        return qq{<script src="$js_uri_path_validate"></script>
+<script>
+document.validation = $json;
+if (document.check_form) document.check_form("$form_name");
+</script>
+};
 
-  my $str = &YAML::Dump((scalar keys %EXTRA) ? (\%EXTRA) : () , $val_hash);
-  $str =~ s/(?<!\\)\\(?=[sSdDwWbB0-9?.*+|\-\^\${}()\[\]])/\\\\/g;
-  $str =~ s/\n/\\n\\\n/g; # allow for one big string
-  $str =~ s/\"/\\\"/g; # quotify it
+    } elsif (eval { require YAML }) {
 
-  ### get the paths
-  my $js_uri_path_yaml = $JS_URI_PATH_YAML || do {
-    die "Missing \$js_uri_path" if ! $js_uri_path;
-    "$js_uri_path/CGI/Ex/yaml_load.js";
-  };
-  my $js_uri_path_validate = $JS_URI_PATH_VALIDATE || do {
-    die "Missing \$js_uri_path" if ! $js_uri_path;
-    "$js_uri_path/CGI/Ex/validate.js";
-  };
+        my $str = YAML::Dump((scalar keys %EXTRA) ? (\%EXTRA) : () , $val_hash);
+        $str =~ s/(?<!\\)\\(?=[sSdDwWbB0-9?.*+|\-\^\${}()\[\]])/\\\\/g; # fix some issues with YAML
+        $str =~ s/\n/\\n\\\n/g; # allow for one big string that flows on multiple lines
+        $str =~ s/\"/\\\"/g; # quotify it
+
+        ### get the paths
+        my $js_uri_path_yaml = $JS_URI_PATH_YAML || do {
+            die "Missing \$js_uri_path" if ! $js_uri_path;
+            "$js_uri_path/CGI/Ex/yaml_load.js";
+        };
 
-  ### return the string
-  return qq{<script src="$js_uri_path_yaml"></script>
+        ### return the string
+        return qq{<script src="$js_uri_path_yaml"></script>
 <script src="$js_uri_path_validate"></script>
-<script><!--
+<script>
 document.validation = "$str";
 if (document.check_form) document.check_form("$form_name");
-//--></script>
+</script>
 };
-
+    } else {
+        return '<!-- no JSON or YAML support found for JS validation -->';
+    }
 }
 
 ###----------------------------------------------------------------###
@@ -878,12 +839,10 @@ use strict;
 use overload '""' => \&as_string;
 
 sub new {
-  my $class  = shift || __PACKAGE__;
-  my $errors = shift;
-  my $extra  = shift || {};
-  die "Missing or invalid arrayref" if ! UNIVERSAL::isa($errors, 'ARRAY');
-  die "Missing or invalid hashref"  if ! UNIVERSAL::isa($extra,  'HASH');
-  return bless {errors => $errors, extra => $extra}, $class;
+    my ($class, $errors, $extra) = @_;
+    die "Missing or invalid errors arrayref" if ref $errors ne 'ARRAY';
+    die "Missing or invalid extra  hashref"  if ref $extra  ne 'HASH';
+    return bless {errors => $errors, extra => $extra}, $class;
 }
 
 sub as_string {
@@ -1115,103 +1074,110 @@ sub get_error_text {
 
 __END__
 
-=head1 NAME
-
-CGI::Ex::Validate - Yet another form validator - does good javascript too
-
-$Id: Validate.pm,v 1.79 2005/02/23 21:28:11 pauls Exp $
-
 =head1 SYNOPSIS
 
-  use CGI::Ex::Validate;
-
-  ### THE SHORT
-
-  my $errobj = CGI::Ex::Validate->new->validate($form, $val_hash);
-
-  ### THE LONG
-
-  my $form = CGI->new;
-   # OR #
-  my $form = CGI::Ex->new; # OR CGI::Ex->get_form;
-   # OR #
-  my $form = {key1 => 'val1', key2 => 'val2'};
+    use CGI::Ex::Validate;
+
+    ### THE SHORT
+
+    my $errobj = CGI::Ex::Validate->new->validate($form, $val_hash);
+
+    ### THE LONG
+
+    my $form = CGI->new;
+     # OR #
+    my $form = CGI::Ex->new; # OR CGI::Ex->get_form;
+     # OR #
+    my $form = {key1 => 'val1', key2 => 'val2'};
+
+
+    ### simplest
+    my $val_hash = {
+        username => {
+            required => 1,
+            max_len  => 30,
+            field    => 'username',
+            # field is optional in this case - will use key name
+        },
+        email    => {
+            required => 1,
+            max_len  => 100,
+            type     => 'email',
+        },
+        email2   => {
+            equals   => 'email',
+        },
+    };
+
+    ### ordered (only onevent submit needs order)
+    my $val_hash = {
+        'group order' => [qw(username email email2)],
+        username => {required => 1, max_len => 30},
+        email    => ...,
+        email2   => ...,
+    };
+
+    ### ordered again
+    my $val_hash = {
+        'group fields' => [{
+            field    => 'username', # field is not optional in this case
+            required => 1,
+            max_len  => 30,
+        }, {
+            field    => 'email',
+            required => 1,
+            max_len  => 100,
+        }, {
+            field    => 'email2',
+            equals   => 'email',
+        }],
+    };
+
+
+    my $vob    = CGI::Ex::Validate->new;
+    my $errobj = $vob->validate($form, $val_hash);
 
+    # OR #
+    # import config using any type CGI::Ex::Conf supports
+    my $errobj = $vob->validate($form, "/somefile/somewhere.val");
 
-  ### simplest
-  my $val_hash = {
-    username => {required => 1,
-                 max_len  => 30
-                 field    => 'username',
-                 # field is optional in this case - will use key name
-                },
-    email    => {required => 1,
-                 max_len  => 100
-                },
-    email2   => {validate_if => 'email'
-                 equals      => 'email'
-                },
-  };
+    if ($errobj) {
+        my $error_heading = $errobj->as_string; # OR "$errobj";
+        my $error_list    = $errobj->as_array;  # ordered list of what when wrong
+        my $error_hash    = $errobj->as_hash;   # hash of arrayrefs of errors
+    } else {
+        # the form passed validation
+    }
 
-  ### ordered
-  my $val_hash = {
-    'group order' => [qw(username email email2)],
-    username => {required => 1, max_len => 30},
-    email    => ...,
-    email2   => ...,
-  };
+    ### will add an error for any form key not found in $val_hash
+    my $vob = CGI::Ex::Validate->new({no_extra_keys => 1});
+    my $errobj = $vob->validate($form, $val_hash);
 
-  ### ordered again
-  my $val_hash = {
-    'group fields' => [
-      {field    => 'username', # field is not optional in this case
-       required => 1,
-       max_len  => 30,
-      },
-      {field    => 'email',
-       required => 1,
-       max_len  => 100,
-      }
-      {field       => 'email2',
-       validate_if => 'email',
-       equals      => 'email',
-      }
-    ],
-  };
 
-  
-  my $vob    = CGI::Ex::Validate->new;
-  my $errobj = $vob->validate($form, $val_hash);
-    # OR #
-  my $errobj = $vob->validate($form, "/somefile/somewhere.val"); # import config using yaml file
-    # OR #
-  my $errobj = $vob->validate($form, "/somefile/somewhere.pl");  # import config using perl file
-    # OR #
-  my $errobj = $vob->validate($form, "--- # a yaml document\n"); # import config using yaml str
+    my $js_uri_path = '/js/';     # static or dynamic URI path to find CGI/Ex/validate.js
+    my $form_name   = "the_form"; # name of the form to attach javascript to
+    my $javascript  = $vob->generate_js($val_hash, $form_name, $js_uri_path);
 
 
-  if ($errobj) {
-    my $error_heading = $errobj->as_string; # OR "$errobj";
-    my $error_list    = $errobj->as_array;  # ordered list of what when wrong
-    my $error_hash    = $errobj->as_hash;   # hash of arrayrefs of errors
-  } else {
-    # form passed validation
-  }
+=head1 DESCRIPTION
 
-  ### will add an error for any form key not found in $val_hash
-  my $vob = CGI::Ex::Validate->new({no_extra_keys => 1});
-  my $errobj = $vob->validate($form, $val_hash);  
+CGI::Ex::Validate is one of many validation modules.  It aims to have
+all of the basic data validation functions, avoid adding all of the
+millions of possible types, while still giving the capability for the
+developer to add their own types for the rare cases that the basic
+ones don't suffice.  Generally anything more than basic validation
+probably needs programmatic or data based validation.
 
-=head1 DESCRIPTION
+CGI::Ex::Validate also has full support for providing the same
+validation in javascript.  It provides methods for attaching the
+javascript to existing forms.  This ability is tightly integrated into
+CGI::Ex::App, but it should be easy to add validation just about
+anywhere using any type of controller.
 
-CGI::Ex::Validate is yet another module used for validating input.  It
-aims to have all of the power of former modules, while advancing them
-with more flexibility, external validation files, and identical
-javascript validation.  CGI::Ex::Validate can work in a simple way
-like all of the other validators do.  However, it also allows for
-grouping of validation items and conditional validaion of groups or
-individual items.  This is more in line with the normal validation
-procedures for a website.
+As opposed to other kitchen sync validation modules, CGI::Ex::Validate
+offers the simple types of validation, and makes it easy to add your
+own custom types.  Asside from custom and custom_js, all validation
+markup is declarative.
 
 =head1 METHODS
 
@@ -1227,7 +1193,7 @@ or nothing at all.  Keys of the hash become the keys of the object.
 Given a filename or YAML string will return perl hash.  If more than one
 group is contained in the file, it will return an arrayref of hashrefs.
 
-  my $ref = $self->get_validation($file);
+    my $ref = $self->get_validation($file);
 
 =item C<get_validation_keys>
 
@@ -1237,340 +1203,364 @@ check to see if extra items have been passed to validate.  If a second
 argument contains a form hash is passed, get_validation_keys will only
 return the keys of groups that were validated.
 
-  my $key_hashref = $self->get_validation_keys($val_hash);
+    my $key_hashref = $self->get_validation_keys($val_hash);
 
 The values of the hash are the names of the fields.
 
 =item C<validate>
 
-Arguments are a form hashref or cgi object, a validation hashref or filename, and
-an optional what_was_validated arrayref.
-If a CGI object is passed, CGI::Ex::get_form will be called on that object
-to turn it into a hashref.  If a filename is given for the validation, get_validation
-will be called on that filename.  If the what_was_validated_arrayref is passed - it
-will be populated (pushed) with the field hashes that were actually validated (anything
-that was skipped because of validate_if will not be in the array).
+Arguments are a form hashref or cgi object, a validation hashref or
+filename, and an optional what_was_validated arrayref (discussed
+further later on).  If a CGI object is passed, CGI::Ex::get_form will
+be called on that object to turn it into a hashref.  If a filename is
+given for the validation, get_validation will be called on that
+filename.  If the what_was_validated_arrayref is passed - it will be
+populated (pushed) with the field hashes that were actually validated
+(anything that was skipped because of validate_if will not be in the
+array).
 
-If the form passes validation, validate will return undef.  If it fails validation, it
-will return a CGI::Ex::Validate::Error object.  If the 'raise_error' general option
-has been set, validate will die with a CGI::Ex::validate::Error object as the value.
+If the form passes validation, validate will return undef.  If it
+fails validation, it will return a CGI::Ex::Validate::Error object.
+If the 'raise_error' option has been set, validate will die
+with a CGI::Ex::validate::Error object as the value.
 
-  my $err_obj = $self->validate($form, $val_hash);
+    my $err_obj = $self->validate($form, $val_hash);
 
     # OR #
 
-  $self->{raise_error} = 1; # raise error can also be listed in the val_hash
-  eval { $self->validate($form, $val_hash) };
-  if ($@) {
-    my $err_obj = $@;
-  }
+    $self->{raise_error} = 1; # can also be listed in the val_hash
+    eval { $self->validate($form, $val_hash) };
+    if ($@) { my $err_obj = $@; }
 
 =item C<generate_js>
 
-Requires YAML to work properly (see L<YAML>).
+Works with CGI::Ex::JSONDump, but can also work with  JSON or YAML
+if desired (see L<JSON> or L<YAML>).
 
 Takes a validation hash, a form name, and an optional javascript uri
 path and returns Javascript that can be embedded on a page and will
-perform identical validations as the server side.  The validation can
-be any validation hash (or arrayref of hashes.  The form name must be
+perform identical validations as the server side.  The form name must be
 the name of the form that the validation will act upon - the name is
 used to register an onsubmit function.  The javascript uri path is
-used to embed the locations two external javascript source files.
-
+used to embed the locations of javascript source files included
+with the CGI::Ex distribution.
 
 The javascript uri path is highly dependent upon the server
-implementation and therefore must be configured manually.  It may be
+configuration and therefore must be configured manually.  It may be
 passed to generate_js, or it may be specified in $JS_URI_PATH.  There
 are two files included with this module that are needed -
 CGI/Ex/yaml_load.js and CGI/Ex/validate.js.  When generating the js
 code, generate_js will look in $JS_URI_PATH_YAML and
 $JS_URI_PATH_VALIDATE.  If either of these are not set, generate_js
 will default to "$JS_URI_PATH/CGI/Ex/yaml_load.js" and
-"$JS_URI_PATH/CGI/Ex/validate.js".
-
-  $self->generate_js($val_hash, 'my_form', "/cgi-bin/js")
-  # would generate something like the following...
-  # <script src="/cgi-bin/js/CGI/Ex/yaml_load.js"></script>
-  # <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
-  # ... more js follows ...
-
-  $CGI::Ex::Validate::JS_URI_PATH      = "/stock/js";
-  $CGI::Ex::Validate::JS_URI_PATH_YAML = "/js/yaml_load.js";
-  $self->generate_js($val_hash, 'my_form')  
-  # would generate something like the following...
-  # <script src="/js/yaml_load.js"></script>
-  # <script src="/stock/js/CGI/Ex/validate.js"></script>
-  # ... more js follows ...
+"$JS_URI_PATH/CGI/Ex/validate.js" (Note: yaml_load is only needed
+if the flags no_jsondump and no_json have been set).
+
+    $self->generate_js($val_hash, 'my_form', "/cgi-bin/js")
+
+    # would generate something like the following...
+
+    <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
+    ... more js follows ...
+
+    $CGI::Ex::Validate::JS_URI_PATH = "/stock/js";
+    $self->generate_js($val_hash, 'my_form')
+
+    # would generate something like the following...
+
+    <script src="/stock/js/CGI/Ex/validate.js"></script>
+    ... more js follows ...
 
 Referencing yaml_load.js and validate.js can be done in any of
 several ways.  They can be copied to or symlinked to a fixed location
-in the servers html directory.  They can also be printed out by a cgi.
+in the server's html directory.  They can also be printed out by a cgi.
 The method C<-E<gt>print_js> has been provided in CGI::Ex for printing
-js files found in the perl heirchy.  See L<CGI::Ex> for more details.
+js files found in the perl hierarchy.  See L<CGI::Ex> for more details.
 The $JS_URI_PATH of "/cgi-bin/js" could contain the following:
 
-  #!/usr/bin/perl -w
+    #!/usr/bin/perl -w
 
-  use strict;
-  use CGI::Ex;
+    use strict;
+    use CGI::Ex;
 
-  ### path_info should contain something like /CGI/Ex/yaml_load.js
-  my $info = $ENV{PATH_INFO} || '';
-  die "Invalid path" if $info !~ m|^(/\w+)+.js$|;
-  $info =~ s|^/+||;
+    ### path_info should contain something like /CGI/Ex/validate.js
+    my $info = $ENV{PATH_INFO} || '';
+    die "Invalid path" if $info !~ m|^(/\w+)+.js$|;
+    $info =~ s|^/+||;
 
-  CGI::Ex->new->print_js($info);
-  exit;
+    CGI::Ex->new->print_js($info);
+    exit;
 
 The print_js method in CGI::Ex is designed to cache the javascript in
-the browser (caching is suggested as they are medium sized files).
+the browser.
 
 =item C<-E<gt>cgix>
 
-Returns a CGI::Ex object.  Used internally.
-
-=item C<-E<gt>conf>
-
-Returns a CGI::Ex::Conf object.  Used internally.
+Returns a CGI::Ex object.  Used internally if a CGI object is
+passed to validate rather than a straight form hash.
 
 =back
 
 =head1 VALIDATION HASH
 
-The validation hash may be passed as a perl a hashref or 
-as a filename, or as a YAML document string.  If it is a filename,
-it will be translated into a hash using the %EXT_HANDLER for the
-extension on the file.  If there is no extension, it will use $DEFAULT_EXT
-as a default.
-
-The validation hash may also be an arrayref of hashrefs.  In this
-case, each arrayref is treated as a group and is validated separately.
+The validation hash may be passed as a hashref or as a filename, or as
+a YAML document string.  Experience has shown it to be better
+programming to pass in a hashref.  If the validation "hash" is a
+filename or a YAML string, it will be translated into a hash using
+CGI::Ex::Conf.
 
-=head1 GROUPS
+Keys matching the regex m/^(general|group)\s+(\w+)$/ such as "group
+onevent" are reserved and are counted as GROUP OPTIONS.  Other keys
+(if any, should be field names that need validation).
 
-Each hashref that is passed as a validation hash is treated as a
-group.  Keys matching the regex m/^group\s+(\w+)$/ are reserved and
-are counted as GROUP OPTIONS.  Keys matching the regex m/^general\s+(\w+)$/
-are reserved and are counted as GENERAL OPTIONS.  Other keys (if
-any, should be keys that need validation).
+If the GROUP OPTION 'group validate_if' is set, the validation will
+only be validated if the conditions of the validate_if are met.  If
+'group validate_if' is not specified, then the validation will
+proceed.  See the validate_if VALIDATION type for more information.
 
-If the GROUP OPTION 'group validate_if' is set, the group will only
-be validated if the conditions are met.  Any group with out a validate_if
-fill be automatically validated.
-
-Each of the items listed in the group will be validated.  The
+Each of the items listed in the validation will be validated.  The
 validation order is determined in one of three ways:
 
 =over 4
 
 =item Specify 'group fields' arrayref.
 
-  # order will be (username, password, 'm/\w+_foo/', somethingelse)
-  {
-    'group title' => "User Information",
-    'group fields' => [
-      {field => 'username',   required => 1},
-      {field => 'password',   required => 1},
-      {field => 'm/\w+_foo/', required => 1},
-    ],
-    somethingelse => {required => 1},
-  }
+    # order will be (username, password, 'm/\w+_foo/', somethingelse)
+    {
+      'group title' => "User Information",
+      'group fields' => [
+        {field => 'username',   required => 1},
+        {field => 'password',   required => 1},
+        {field => 'm/\w+_foo/', required => 1},
+      ],
+      somethingelse => {required => 1},
+    }
 
 =item Specify 'group order' arrayref.
 
-  # order will be (username, password, 'm/\w+_foo/', somethingelse)
-  {
-    'group title' => "User Information",
-    'group order' => [qw(username password), 'm/\w+_foo/'],
-    username      => {required => 1},
-    password      => {required => 1},
-    'm/\w+_foo/'  => {required => 1},
-    somethingelse => {required => 1},
-  }
+    # order will be (username, password, 'm/\w+_foo/', somethingelse)
+    {
+      'group title' => "User Information",
+      'group order' => [qw(username password), 'm/\w+_foo/'],
+      username      => {required => 1},
+      password      => {required => 1},
+      'm/\w+_foo/'  => {required => 1},
+      somethingelse => {required => 1},
+    }
 
 =item Do nothing - use sorted order.
 
-  # order will be ('m/\w+_foo/', password, somethingelse, username)
-  {
-    'group title' => "User Information",
-    username      => {required => 1},
-    password      => {required => 1},
-    'm/\w+_foo/'  => {required => 1},
-    somethingelse => {required => 1},
-  }
+    # order will be ('m/\w+_foo/', password, somethingelse, username)
+    {
+      'group title' => "User Information",
+      username      => {required => 1},
+      password      => {required => 1},
+      'm/\w+_foo/'  => {required => 1},
+      somethingelse => {required => 1},
+    }
 
 =back
 
-Each of the individual field validation hashrefs should contain
-the types listed in VALIDATION TYPES.
+Optionally the 'group fields' or the 'group order' may contain the
+word 'OR' as a special keyword.  If the item preceding 'OR' fails
+validation the item after 'OR' will be tested instead.  If the item
+preceding 'OR' passes validation the item after 'OR' will not be
+tested.
 
-Optionally the 'group fields' or the 'group order' may contain the word
-'OR' as a special keyword.  If the item preceding 'OR' fails validation
-the item after 'OR' will be tested instead.  If the item preceding 'OR'
-passes validation the item after 'OR' will not be tested.
+    'group order' => [qw(zip OR postalcode state OR region)],
+
+At this time, only "group onevent" submit works with this option.  Using
+OR is deprecated.  Instead you should use min_in_set or max_in_set.
 
-  'group order' => [qw(zip OR postalcode state OR region)],
+    'zip' => {
+         max_in_set: '1 of zip, postalcode',
+    },
+    'state' => {
+         max_in_set: '1 of state, region',
+    },
 
-Each individual validation hashref will operate on the field contained
+Each individual field validation hashref will operate on the field contained
 in the 'field' key.  This key may also be a regular expression in the
 form of 'm/somepattern/'.  If a regular expression is used, all keys
-matching that pattern will be validated.
-
-=head1 VALIDATION TYPES
+matching that pattern will be validated.  If the field key is
+not specified, the key from the top level hash will be used.
 
-The following are the available validation types.  Multiple instances of
-the same type may be used by adding a number to the type (ie match, match2,
-match232, match_94).  Multiple instances are validated in sorted order.
-
-=over 4
+    foobar => {   # "foobar" is not used as key because field is specified
+       field    => 'real_key_name',
+       required => 1,
+    },
+    real_key_name2 => {
+       required => 1,
+    },
 
-=item C<validate_if>
+Each of the individual field validation hashrefs should contain the
+types listed in VALIDATION TYPES.
 
-If validate_if is specified, the field will only be validated
-if the conditions are met.  Works in JS.
+=head1 VALIDATION TYPES
 
-  validate_if => {field => 'name', required => 1, max_len => 30}
-  # Will only validate if the field "name" is present and is less than 30 chars.
+This section lists the available validation types.  Multiple instances
+of the same type may be used for some validation types by adding a
+number to the type (ie match, match2, match232).  Multiple instances
+are validated in sorted order.  Types that allow multiple values are:
+compare, custom, custom_js, equals, enum, match, required_if, sql,
+type, validate_if, and replace (replace is a MODIFICATION TYPE).
 
-  validate_if => 'name',
-  # SAME as
-  validate_if => {field => 'name', required => 1},
+=over 4
 
-  validate_if => '! name',
-  # SAME as
-  validate_if => {field => 'name', max_in_set => '0 of name'},
+=item C<compare>
 
-  validate_if => {field => 'country', compare => "eq US"},
-  # only if country's value is equal to US
+Allows for custom comparisons.  Available types are
+>, <, >=, <=, !=, ==, gt, lt, ge, le, ne, and eq.  Comparisons
+also work in the JS.
 
-  validate_if => {field => 'country', compare => "ne US"},
-  # if country doesn't equal US
+    {
+      field    => 'my_number',
+      match    => 'm/^\d+$/',
+      compare1 => '> 100',
+      compare2 => '< 255',
+      compare3 => '!= 150',
+    }
 
-  validate_if => {field => 'password', match => 'm/^md5\([a-z0-9]{20}\)$/'},
-  # if password looks like md5(12345678901234567890)
+=item C<custom>
 
-  {
-    field       => 'm/^(\w+)_pass/',
-    validate_if => '$1_user',
-    required    => 1,
-  }
-  # will validate foo_pass only if foo_user was present.
+Custom value - not available in JS.  Allows for extra programming types.
+May be either a boolean value predetermined before calling validate, or may be
+a coderef that will be called during validation.  If coderef is called, it will
+be passed the field name, the form value for that name, and a reference to the
+field validation hash.  If the custom type returns false the element fails
+validation and an error is added.
 
-The validate_if may also contain an arrayref of validation items.  So that
-multiple checks can be run.  They will be run in order.  validate_if will
-return true only if all options returned true.
+    {
+      field => 'username',
+      custom => sub {
+        my ($key, $val, $type, $field_val_hash) = @_;
+        # do something here
+        return 0;
+      },
+    }
 
-  validate_if => ['email', 'phone', 'fax']
+=item C<custom_js>
 
-Optionally, if validate_if is an arrayref, it may contain the word
-'OR' as a special keyword.  If the item preceding 'OR' fails validation
-the item after 'OR' will be tested instead.  If the item preceding 'OR'
-passes validation the item after 'OR' will not be tested.
+Custom value - only available in JS.  Allows for extra programming types.
+May be a javascript function (if fully declared in javascript), a string containing
+a javascript function (that will be eval'ed into a real function),
+a boolean value pre-determined before calling validate, or may be
+section of javascript that will be eval'ed (the last value of
+the eval'ed javascript will determine if validation passed).  A false response indicates
+the value did not pass validation.  A true response indicates that it did.  See
+the samples/validate_js_0_tests.html page for a sample of usages.
+
+    {
+      field => 'date',
+      required => 1,
+      match    => 'm|^\d\d\d\d/\d\d/\d\d$|',
+      match_error => 'Please enter date in YYYY/MM/DD format',
+      custom_js => "function (args) {
+        var t=new Date();
+        var y=t.getYear()+1900;
+        var m=t.getMonth() + 1;
+        var d=t.getDate();
+        if (m<10) m = '0'+m;
+        if (d<10) d = '0'+d;
+        (args.value > ''+y+'/'+m+'/'+d) ? 1 : 0;
+      }",
+      custom_js_error => 'The date was not greater than today.',
+    }
 
-  validate_if => [qw(zip OR postalcode)],
+=item C<enum>
 
-=item C<required_if>
+Allows for checking whether an item matches a set of options.  In perl
+the value may be passed as an arrayref.  In the conf or in perl the
+value may be passed of the options joined with ||.
 
-Requires the form field if the condition is satisfied.  The conditions
-available are the same as for validate_if.  This is somewhat the same
-as saying:
+    {
+      field => 'password_type',
+      enum  => 'plaintext||crypt||md5', # OR enum => [qw(plaintext crypt md5)],
+    }
 
-  validate_if => 'some_condition',
-  required    => 1
+=item C<equals>
 
-  required_if => 'some_condition',
+Allows for comparison of two form elements.  Can have an optional !.
 
-  {
-    field       => 'm/^(\w+)_pass/',
-    required_if => '$1_user',
-  }
-  
-=item C<required>
+    {
+      field  => 'password',
+      equals => 'password_verify',
+    },
+    {
+      field  => 'domain1',
+      equals => '!domain2', # make sure the fields are not the same
+    }
 
-Requires the form field to have some value.  If the field is not present,
-no other checks will be run.
+=item C<match>
 
-=item C<min_values> and C<max_values>
+Allows for regular expression comparison.  Multiple matches may
+be concatenated with ||.  Available in JS.
 
-Allows for specifying the maximum number of form elements passed.
-max_values defaults to 1 (You must explicitly set it higher
-to allow more than one item by any given name).
+    {
+      field   => 'my_ip',
+      match   => 'm/^\d{1,3}(\.\d{1,3})3$/',
+      match_2 => '!/^0\./ || !/^192\./',
+    }
 
-=item C<min_in_set> and C<max_in_set>
+=item C<max_in_set> and C<min_in_set>
 
 Somewhat like min_values and max_values except that you specify the
 fields that participate in the count.  Also - entries that are not
 defined or do not have length are not counted.  An optional "of" can
-be placed after the number for human readibility.
+be placed after the number for human readability.
 
-  min_in_set => "2 of foo bar baz",
-    # two of the fields foo, bar or baz must be set
-    # same as
-  min_in_set => "2 foo bar baz",
-    # same as 
-  min_in_set => "2 OF foo bar baz",
+    min_in_set => "2 of foo bar baz",
+      # two of the fields foo, bar or baz must be set
+      # same as
+    min_in_set => "2 foo bar baz",
+      # same as
+    min_in_set => "2 OF foo bar baz",
 
-  validate_if => {field => 'whatever', max_in_set => '0 of whatever'},
-    # only run validation if there were zero occurances of whatever
+    validate_if => {field => 'whatever', max_in_set => '0 of whatever'},
+      # only run validation if there were zero occurrences of whatever
 
-=item C<enum>
+=item C<max_len and min_len>
 
-Allows for checking whether an item matches a set of options.  In perl
-the value may be passed as an arrayref.  In the conf or in perl the
-value may be passed of the options joined with ||.
-
-  {
-    field => 'password_type',
-    enum  => 'plaintext||crypt||md5', # OR enum => [qw(plaintext crypt md5)],
-  }
+Allows for check on the length of fields
 
-=item C<equals>
+    {
+      field   => 'site',
+      min_len => 4,
+      max_len => 100,
+    }
 
-Allows for comparison of two form elements.  Can have an optional !.
+=item C<max_values> and C<min_values>
 
-  {
-    field  => 'password',
-    equals => 'password_verify',
-  },
-  {
-    field  => 'domain1',
-    equals => '!domain2', # make sure the fields are not the same
-  }
+Allows for specifying the maximum number of form elements passed.
+max_values defaults to 1 (You must explicitly set it higher
+to allow more than one item by any given name).
 
-=item C<min_len and max_len>
+=item C<required>
 
-Allows for check on the length of fields
+Requires the form field to have some value.  If the field is not present,
+no other checks will be run.
 
-  {
-    field   => 'site',
-    min_len => 4,
-    max_len => 100,
-  }
+=item C<required_if>
 
-=item C<match>
+Requires the form field if the condition is satisfied.  The conditions
+available are the same as for validate_if.  This is somewhat the same
+as saying:
 
-Allows for regular expression comparison.  Multiple matches may
-be concatenated with ||.  Available in JS.
+    validate_if => 'some_condition',
+    required    => 1
 
-  {
-    field   => 'my_ip',
-    match   => 'm/^\d{1,3}(\.\d{1,3})3$/',
-    match_2 => '!/^0\./ || !/^192\./',
-  }
+    required_if => 'some_condition',
 
-=item C<compare>
+If a regex is used for the field name, the required_if
+field will have any match patterns swapped in.
 
-Allows for custom comparisons.  Available types are
->, <, >=, <=, !=, ==, gt, lt, ge, le, ne, and eq.  Comparisons
-also work in the JS.
+    {
+      field       => 'm/^(\w+)_pass/',
+      required_if => '$1_user',
+    }
 
-  {
-    field    => 'my_number',
-    match    => 'm/^\d+$/',
-    compare1 => '> 100',
-    compare2 => '< 255',
-    compare3 => '!= 150',
-  }
+This example would require the "foobar_pass" field to be set
+if the "foobar_user" field was passed.
 
 =item C<sql>
 
@@ -1579,67 +1569,69 @@ for in the value $self->{dbhs}->{foo} if sql_db_type is set to 'foo',
 otherwise it will default to $self->{dbh}.  If $self->{dbhs}->{foo} or
 $self->{dbh} is a coderef - they will be called and should return a dbh.
 
-  {
-    field => 'username',
-    sql   => 'SELECT COUNT(*) FROM users WHERE username = ?',
-    sql_error_if => 1, # default is 1 - set to 0 to negate result
-    # sql_db_type  => 'foo', # will look for a dbh under $self->{dbhs}->{foo}
-  }
+    {
+      field => 'username',
+      sql   => 'SELECT COUNT(*) FROM users WHERE username = ?',
+      sql_error_if => 1, # default is 1 - set to 0 to negate result
+      # sql_db_type  => 'foo', # will look for a dbh under $self->{dbhs}->{foo}
+    }
 
-=item C<custom>
+=item C<type>
 
-Custom value - not available in JS.  Allows for extra programming types.
-May be either a boolean value predetermined before calling validate, or may be
-a coderef that will be called during validation.  If coderef is called, it will
-be passed the field name, the form value for that name, and a reference to the
-field validation hash.  If the custom type returns false the element fails
-validation and an error is added.
+Allows for more strict type checking.  Currently supported types
+include CC (credit card), EMAIL, DOMAIN, IP, URL.  Other types will be
+added upon request provided we can add a perl and a javascript
+version.
 
-  {
-    field => 'username',
-    custom => sub {
-      my ($key, $val, $type, $field_val_hash) = @_;
-      # do something here
-      return 0;
-    },
-  }
+    {
+      field => 'credit_card',
+      type  => 'CC',
+    }
 
-=item C<custom_js>
+=item C<validate_if>
 
-Custom value - only available in JS.  Allows for extra programming types.
-May be either a boolean value predermined before calling validate, or may be
-section of javascript that will be eval'ed.  The last value (return value) of
-the eval'ed javascript will determine if validation passed.  A false value indicates
-the value did not pass validation.  A true value indicates that it did.  See
-the t/samples/js_validate_3.html page for a sample of usage.
-
-  {
-    field => 'date',
-    required => 1,
-    match    => 'm|^\d\d\d\d/\d\d/\d\d$|',
-    match_error => 'Please enter date in YYYY/MM/DD format',
-    custom_js => "
-      var t=new Date();
-      var y=t.getYear()+1900;
-      var m=t.getMonth() + 1;
-      var d=t.getDate();
-      if (m<10) m = '0'+m;
-      if (d<10) d = '0'+d;
-      (value > ''+y+'/'+m+'/'+d) ? 1 : 0;
-    ",
-    custom_js_error => 'The date was not greater than today.',
-  }
+If validate_if is specified, the field will only be validated
+if the conditions are met.  Works in JS.
 
-=item C<type>
-Allows for more strict type checking.  Many types will be added and
-will be available from javascript as well.  Currently support types
-are CC.
-
-  {
-    field => 'credit_card',
-    type  => 'CC',
-  }
+    validate_if => {field => 'name', required => 1, max_len => 30}
+    # Will only validate if the field "name" is present and is less than 30 chars.
+
+    validate_if => 'name',
+    # SAME as
+    validate_if => {field => 'name', required => 1},
+
+    validate_if => '! name',
+    # SAME as
+    validate_if => {field => 'name', max_in_set => '0 of name'},
+
+    validate_if => {field => 'country', compare => "eq US"},
+    # only if country's value is equal to US
+
+    validate_if => {field => 'country', compare => "ne US"},
+    # if country doesn't equal US
+
+    validate_if => {field => 'password', match => 'm/^md5\([a-z0-9]{20}\)$/'},
+    # if password looks like md5(12345678901234567890)
+
+    {
+      field       => 'm/^(\w+)_pass/',
+      validate_if => '$1_user',
+      required    => 1,
+    }
+    # will validate foo_pass only if foo_user was present.
+
+The validate_if may also contain an arrayref of validation items.  So that
+multiple checks can be run.  They will be run in order.  validate_if will
+return true only if all options returned true.
+
+    validate_if => ['email', 'phone', 'fax']
+
+Optionally, if validate_if is an arrayref, it may contain the word
+'OR' as a special keyword.  If the item preceding 'OR' fails validation
+the item after 'OR' will be tested instead.  If the item preceding 'OR'
+passes validation the item after 'OR' will not be tested.
+
+    validate_if => [qw(zip OR postalcode)],
 
 =back
 
@@ -1672,47 +1664,50 @@ a different field.  If the field name was a regex, any patterns will
 be swapped into the delegate_error value. This option is generally only
 useful with the as_hash method of the error object (for inline errors).
 
-  {
-    field => 'zip',
-    match => 'm/^\d{5}/',
-  },
-  {
-    field => 'zip_plus4',
-    match => 'm/^\d{4}/',
-    delegate_error => 'zip',
-  },
-
-  {
-    field => 'm/^(id_[\d+])_user$/',
-    delegate_error => '$1',
-  },
+    {
+      field => 'zip',
+      match => 'm/^\d{5}/',
+    },
+    {
+      field => 'zip_plus4',
+      match => 'm/^\d{4}/',
+      delegate_error => 'zip',
+    },
+    {
+      field => 'm/^(id_[\d+])_user$/',
+      delegate_error => '$1',
+    },
 
 =item C<exclude_js>
 
 This allows the cgi to do checking while keeping the checks from
 being run in JavaScript
 
-  {
-    field      => 'cgi_var',
-    required   => 1,
-    exclude_js => 1,
-  }
+    {
+      field      => 'cgi_var',
+      required   => 1,
+      exclude_js => 1,
+    }
 
 =item C<exclude_cgi>
 
 This allows the js to do checking while keeping the checks from
 being run in the cgi
 
-  {
-    field       => 'js_var',
-    required    => 1,
-    exclude_cgi => 1,
-  }
+    {
+      field       => 'js_var',
+      required    => 1,
+      exclude_cgi => 1,
+    }
 
 =back
 
 =head1 MODIFYING VALIDATION TYPES
 
+The following types will modify the form value before it is processed.
+They work in both the perl and in javascript as well.  The javascript
+version changes the actual value in the form on appropriate form types.
+
 =over 4
 
 =item C<do_not_trim>
@@ -1721,22 +1716,29 @@ By default, validate will trim leading and trailing whitespace
 from submitted values.  Set do_not_trim to 1 to allow it to
 not trim.
 
-  {field => 'foo', do_not_trim => 1}
+    {field => 'foo', do_not_trim => 1}
+
+=item C<trim_control_chars>
+
+Off by default.  If set to true, removes characters in the
+\x00 to \x31 range (Tabs are translated to a single space).
+
+    {field => 'foo', trim_control_chars => 1}
 
 =item C<replace>
 
 Pass a swap pattern to change the actual value of the form.
-Any perl regex can be passed.
+Any perl regex can be passed but it is suggested that javascript
+compatible regexes are used to make generate_js possible.
 
-  {field => 'foo', replace => 's/(\d{3})(\d{3})(\d{3})/($1) $2-$3/'}
+    {field => 'foo', replace => 's/(\d{3})(\d{3})(\d{3})/($1) $2-$3/'}
 
 =item C<default>
 
 Set item to default value if there is no existing value (undefined
-or zero length string).  Maybe someday well add default_if (but that
-would require some odd syntax for both the conditional and the default).
+or zero length string).
 
-  {field => 'country', default => 'EN'}
+    {field => 'country', default => 'EN'}
 
 =item C<to_upper_case> and C<to_lower_case>
 
@@ -1748,14 +1750,19 @@ Requires that the validated field has been also checked with
 an enum, equals, match, compare, custom, or type check.  If the
 field has been checked and there are no errors - the field is "untainted."
 
-This is for use in conjunction with the -T switch.
+This is for use in conjunction with perl's -T switch.
+
+=item C<clear_on_error>
+
+Clears the form field should a validation error occur.  Only supported
+on the Javascript side (no affect on the server side).
 
 =back
 
 =head1 ERROR OBJECT
 
-Failed validation results in an error object blessed into the class found in
-$ERROR_PACKAGE - which defaults to CGI::Ex::Validate::Error.
+Failed validation results in an error an error object created via the
+new_error method.  The default error class is CGI::Ex::Validate::Error.
 
 The error object has several methods for determining what the errors were.
 
@@ -1764,10 +1771,10 @@ The error object has several methods for determining what the errors were.
 =item C<as_array>
 
 Returns an array or arrayref (depending on scalar context) of errors that
-occurred in the order that they occured.  Individual groups may have a heading
+occurred in the order that they occurred.  Individual groups may have a heading
 and the entire validation will have a heading (the default heading can be changed
-via the 'as_array_title' general option).  Each error that occured is a separate
-item and are prepended with 'as_array_prefix' (which is a general option - default
+via the 'as_array_title' group option).  Each error that occurred is a separate
+item and are pre-pended with 'as_array_prefix' (which is a group option - default
 is '  ').  The as_array_ options may also be set via a hashref passed to as_array.
 as_array_title defaults to 'Please correct the following items:'.
 
@@ -1789,8 +1796,8 @@ as_array_title defaults to 'Please correct the following items:'.
 Returns values of as_array joined with a newline.  This method is used as
 the stringification for the error object.  Values of as_array are joined with
 'as_string_join' which defaults to "\n".  If 'as_string_header' is set, it will
-be prepended onto the error string.  If 'as_string_footer' is set, it will be
-postpended onto the error string.
+be pre-pended onto the error string.  If 'as_string_footer' is set, it will be
+appended onto the error string.
 
   ### if this returns the following
   my $string = $err_obj->as_string;
@@ -1811,18 +1818,20 @@ postpended onto the error string.
 =item C<as_hash>
 
 Returns a hash or hashref (depending on scalar context) of errors that
-occurred.   Each key is the field name of the form that failed validation with
-'as_hash_suffix' added on as a suffix.  as_hash_suffix is available as a general option
-and may also be passed in via a hashref as the only argument to as_hash.
-The default value is '_error'.  The values of the hash are arrayrefs of errors
-that occured to that form element.
-
-By default as_hash will return the values of the hash as arrayrefs (a list of the errors
-that occured to that key).  It is possible to also return the values as strings.
-Three options are available for formatting: 'as_hash_header' which will be prepended
-onto the error string, 'as_hash_footer' which will be postpended, and 'as_hash_join' which
-will be used to join the arrayref.  The only argument required to force the
-stringification is 'as_hash_join'.
+occurred.  Each key is the field name of the form that failed
+validation with 'as_hash_suffix' added on as a suffix.  as_hash_suffix
+is available as a group option and may also be passed in via a
+hashref as the only argument to as_hash.  The default value is
+'_error'.  The values of the hash are arrayrefs of errors that
+occurred to that form element.
+
+By default as_hash will return the values of the hash as arrayrefs (a
+list of the errors that occurred to that key).  It is possible to also
+return the values as strings.  Three options are available for
+formatting: 'as_hash_header' which will be pre-pended onto the error
+string, 'as_hash_footer' which will be appended, and 'as_hash_join'
+which will be used to join the arrayref.  The only argument required
+to force the stringification is 'as_hash_join'.
 
   ### if this returns the following
   my $hash = $err_obj->as_hash;
@@ -1843,67 +1852,62 @@ stringification is 'as_hash_join'.
 
 =head1 GROUP OPTIONS
 
-Any key in a validation hash matching the pattern m/^group\s+(\w+)$/
-is considered a group option.  The current know options are:
+Any key in a validation hash matching the pattern
+m/^(group|general)\s+(\w+)$/ is considered a group option (the reason
+that either group or general may be used is that CGI::Ex::Validate
+used to have the concept of validation groups - these were not
+commonly used so support has been deprecated as of the 2.10 release).
+Group options will also be looked for in the Validate object ($self)
+and can be set when instantiating the object ($self->{raise_error} is
+equivalent to $valhash->{'group raise_error'}).
+
+Options may also be set globally before calling validate by
+populating the %DEFAULT_OPTIONS global hash.  However, only the options
+set properly in the $valhash will be passed to the javascript.
 
 =over 4
 
-=item C<'group title'>
+=item C<title>
 
 Used as a group section heading when as_array or as_string is called
 by the error object.
 
-=item C<'group order'>
-
-Order in which to validate key/value pairs of group.
-
-=item C<'group fields'>
+    'group title' => 'Title of errors',
 
-Arrayref of validation items to validate.
-
-=item C<'group validate_if'>
-
-Conditions that will be checked to see if the group should be validated.
-If no validate_if option is found, the group will be validated.
-
-=back
+=item C<order>
 
-=head1 GENERAL OPTIONS
+Order in which to validate key/value pairs of group.
 
-Any key in a validation hash matching the pattern m/^general\s+(\w+)$/
-is considered a general option.  General options will also be looked
-for in the Validate object ($self) and can be set when instantiating
-the object ($self->{raise_error} is equivalent to
-$valhash->{'general raise_error'}).  The current know options are:
+    'group order' => [qw(user pass email OR phone)],
 
-General options may be set in any group using the syntax:
+=item C<fields>
 
-  'general general_option_name' => 'general_option_value'
+Arrayref of validation items to validate.
 
-They will only be set if the group's validate_if is successful or
-if the group does not have a validate_if.  It is also possible to set
-a "group general" option using the following syntax:
+    'group fields' => [{
+        field    => 'field1',
+        required => 1,
+    }, {
+        field    => 'field2',
+        required => 1,
+    }],
 
-  'group general_option_name' => 'general_option_value'
+=item C<validate_if>
 
-These items will only be set if the group fails validation.
-If a group has a validate_if block and passes validation, the group
-items will not be used.  This is so that a failed section can have
-its own settings.  Note though that the last option found will be
-used and that items set in $self override those set in the validation
-hash.
+If specified - the entire hashref will only be validated if
+the "if" conditions are met.
 
-Options may also be set globally before calling validate by
-populating the %DEFAULT_OPTIONS global hash.
+    'group validate_if => {field => 'email', required => 1},
 
-=over 4
+This group would only validate all fields if the email field
+was present.
 
-=item C<'general raise_error'>
+=item C<raise_error>
 
 If raise_error is true, any call to validate that fails validation
 will die with an error object as the value.
 
-=item C<'general no_extra_fields'>
+=item C<no_extra_fields>
 
 If no_extra_fields is true, validate will add errors for any field found
 in form that does not have a field_val hashref in the validation hash.
@@ -1914,101 +1918,138 @@ An important exception to this is that field_val hashrefs or field names listed
 in a validate_if or required_if statement will not be included.  You must
 have an explicit entry for each key.
 
-=item C<'general \w+_error'>
+=item C<\w+_error>
 
 These items allow for an override of the default errors.
 
-  'general required_error' => '$name is really required',
-  'general max_len_error'  => '$name must be shorter than $value characters',
+  'group required_error' => '$name is really required',
+  'group max_len_error'  => '$name must be shorter than $value characters',
     # OR #
   my $self = CGI::Ex::Validate->new({
     max_len_error => '$name must be shorter than $value characters',
   });
 
-=item C<'general as_array_title'>
+=item C<as_array_title>
 
 Used as the section title for all errors that occur, when as_array
 or as_string is called by the error object.
 
-=item C<'general as_array_prefix'>
+=item C<as_array_prefix>
 
 Used as prefix to individual errors that occur, when as_array
 or as_string is called by the error object.  Each individual error
 will be prefixed with this string.  Headings will not be prefixed.
 Default is '  '.
 
-=item C<'general as_string_join'>
+=item C<as_string_join>
 
 When as_string is called, the values from as_array will be joined with
 as_string_join.  Default value is "\n".
 
-=item C<'general as_string_header'>
+=item C<as_string_header>
 
-If set, will be prepended onto the string when as_string is called.
+If set, will be pre-pended onto the string when as_string is called.
 
-=item C<'general as_string_footer'>
+=item C<as_string_footer>
 
-If set, will be prepended onto the string when as_string is called.
+If set, will be pre-pended onto the string when as_string is called.
 
-=item C<'general as_hash_suffix'>
+=item C<as_hash_suffix>
 
 Added on to key names during the call to as_hash.  Default is '_error'.
 
-=item C<'general as_hash_join'>
+=item C<as_hash_join>
 
 By default, as_hash will return hashref values that are errors joined with
 the default as_hash_join value of <br />.  It can also return values that are
 arrayrefs of the errors.  This can be done by setting as_hash_join to a non-true value
 (for example '')
 
-=item C<'general as_hash_header'>
+=item C<as_hash_header>
 
 If as_hash_join has been set to a true value, as_hash_header may be set to
-a string that will be prepended on to the error string.
+a string that will be pre-pended on to the error string.
 
-=item C<'general as_hash_footer'>
+=item C<as_hash_footer>
 
 If as_hash_join has been set to a true value, as_hash_footer may be set to
 a string that will be postpended on to the error string.
 
-=item C<'general no_inline'>
+=item C<onevent>
+
+Defaults to {submit => 1}.  This controls when the javascript validation
+will take place.  May be passed any or all or submit, change, or blur.
+Multiple events may be passed in the hash.
+
+    'group onevent' => {submit => 1, change => 1}',
+
+A comma separated string of types may also be passed:
+
+    'group onevent' => 'submit,change,blur',
+
+Currently, change and blur will not work for dynamically matched
+field names such as 'm/\w+/'.  Support will be added.
+
+=item C<set_hook>
 
-If set to true, the javascript validation will not attempt to generate inline
-errors.  Default is true.  Inline errors are independent of confirm and alert
-errors.
+Defaults document.validate_set_hook which defaults to nothing.  If
+"group set_hook" or document.validate_set_hook are set to a function,
+they will be passed the key name of a form element that had a
+validation error and the error that will be set.  If a true value is
+returned, then validate will not also the inline error.  If no value
+or false is returned (default) the validate will continue setting the
+inline error.  This gives full control over setting inline
+errors. samples/validate_js_2_onchange.html has a good example of
+using these hooks.
 
-=item C<'general no_confirm'>
+    'group set_hook' => "function (key, val, val_hash, form) {
+      alert("Setting error to field "+key);
+    }",
 
-If set to true, the javascript validation will try to use an alert instead
-of a confirm to inform the user of errors.  Alert and confirm are independent
+The document.validate_set_hook option is probably the better option to use,
+as it helps to separate display functionality out into your html templates
+rather than storing too much html logic in your CGI.
+
+=item C<clear_hook>
+
+Similar to set_hook, but called when inline error is cleared.  Its
+corresponding default is document.validate_clear_hook.  The clear hook
+is also sampled in samples/validate_js_2_onchange.html
+
+    'group clear_hook' => "function (key, val_hash, form) {
+      alert("Clear error on field "+key);
+    }",
+
+=item C<no_inline>
+
+If set to true, the javascript validation will not attempt to generate
+inline errors when the only "group onevent" type is "submit".  Default
+is true.  Inline errors are independent of confirm and alert errors.
+
+    'group no_inline' => 1,
+
+=item C<no_confirm>
+
+If set to true, the javascript validation will try to use an alert
+instead of a confirm to inform the user of errors when one of the
+"group onevent" types is "submit".  Alert and confirm are independent
 or inline errors.  Default is false.
 
-=item C<'general no_alert'>
+    'group no_confirm' => 1,
+
+=item C<no_alert>
 
 If set to true, the javascript validation will not show an alert box
 when errors occur.  Default is false.  This option only comes into
-play if no_confirm is also set.  This option is independent of inline
-errors.  Although it is possible to turn off all errors by setting
-no_inline, no_confirm, and no_alert all to 1, it is suggested that at
-least one of the error reporting facilities is left on.
-
-=back
+play if no_confirm is also set.  This option is only in effect if
+"group onevent" includes "submit".  This option is independent of
+inline errors.  Although it is possible to turn off all errors by
+setting no_inline, no_confirm, and no_alert all to 1, it is suggested
+that at least one of the error reporting facilities is left on.
 
-It is possible to have a group that contains nothing but general options.
+    'group no_alert' => 1,
 
-  my $val_hash = [
-    {'general error_title'    => 'The following things went wrong',
-     'general error_prefix'   => '  - ',
-     'general raise_error'    => 1,
-     'general name_suffix'    => '_foo_error',
-     'general required_error' => '$name is required',
-    },
-    {'group title' => 'User Information',
-     username => {required => 1},
-     email    => {required => 1},
-     password => {required => 1},
-    },
-  ];
+=back
 
 =head1 JAVASCRIPT
 
@@ -2016,7 +2057,7 @@ CGI::Ex::Validate provides for having duplicate validation on the
 client side as on the server side.  Errors can be shown in any
 combination of inline and confirm, inline and alert, inline only,
 confirm only, alert only, and none.  These combinations are controlled
-by the general options no_inline, no_confirm, and no_alert.
+by the group options no_inline, no_confirm, and no_alert.
 Javascript validation can be generated for a page using the
 C<-E<gt>generate_js> Method of CGI::Ex::Validate.  It is also possible
 to store the validation inline with the html.  This can be done by
@@ -2027,9 +2068,31 @@ validation will be read in using CGI::Ex::Conf::read_handler_html.
 
 All inline html validation must be written in yaml.
 
-It is anticipated that the html will contain something like either of the
+It is anticipated that the html will contain something like one of the
 following examples:
 
+  <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
+  <script>
+  document.validation = {
+    'group no_confirm': 1,
+    'group no_alert':   1,
+    'group onevent':    'change,blur,submit',
+    'group order': ['username', 'password'],
+    username: {
+      required: 1,
+      max_len: 20
+    },
+    password: {
+      required: 1,
+      max_len: 30
+    }
+  };
+  if (document.check_form) document.check_form('my_form_name');
+  </script>
+
+Prior to the realization of JSON, YAML was part of the method
+for introducing validation into the script.
+
   <script src="/cgi-bin/js/CGI/Ex/yaml_load.js"></script>
   <script src="/cgi-bin/js/CGI/Ex/validate.js"></script>
   <script>
@@ -2048,7 +2111,8 @@ following examples:
   if (document.check_form) document.check_form('my_form_name');
   </script>
 
-Alternately we can use element attributes:
+Alternately, CGI/Ex/validate.js can parse the YAML from html
+form element attributes:
 
   <form name="my_form_name">
 
@@ -2074,10 +2138,11 @@ Alternately we can use element attributes:
   if (document.check_form) document.check_form('my_form_name');
   </script>
 
-The read_handler_html from CGI::Ex::Conf will find either of these
-types of validation.
+The read_handler_html from CGI::Ex::Conf will find the YAML types
+of validation.  The JSON type is what would be generated by default
+when the validation is specified in Perl.
 
-If inline errors are asked for, each error that occurs will attempt
+If inline errors are enabled (default), each error that occurs will attempt
 to find an html element with its name as the id.  For example, if
 the field "username" failed validation and created a "username_error",
 the javascript would set the html of <span id="username_error"></span>
@@ -2092,20 +2157,46 @@ from the server side as well.
 If the javascript fails for some reason, the form should still be able
 to submit as normal (fail gracefully).
 
-If the confirm option is used, the errors will be displayed to the user.
-If they choose OK they will be able to try and fix the errors.  If they
-choose cancel, the form will submit anyway and will rely on the server
-to do the validation.  This is for fail safety to make sure that if the
-javascript didn't validate correctly, the user can still submit the data.
+Additionally, there are two hooks that are called when ever an inline
+error is set or cleared.  The following hooks are used in
+samples/validate_js_2_onchange.html.
 
-=head1 AUTHOR
+    document.validate_set_hook = function (key, val, val_hash, form) {
+      document.getElementById(key+'_img').innerHTML
+        = '<span style="font-weight:bold;color:red">!</span>';
+      document.getElementById(key+'_row').style.background
+        = '#ffdddd';
+    };
+
+    document.validate_clear_hook = function (key, val_hash, form) {
+      document.getElementById(key+'_img').innerHTML
+        = '<span style="font-weight:bold;color:green">+</span>';
+      document.getElementById(key+'_row').style.background
+        = '#ddffdd';
+    };
+
+These hooks can also be set as "group clear_hook" and "group set_hook"
+which are defined further above.
 
-Paul Seamons
+If the confirm option is used ("group onevent" includes submit and
+"group no_confirm" is false), the errors will be displayed to the
+user.  If they choose OK they will be able to try and fix the errors.
+If they choose cancel, the form will submit anyway and will rely on
+the server to do the validation.  This is for fail safety to make sure
+that if the javascript didn't validate correctly, the user can still
+submit the data.
+
+=head1 THANKS
+
+Thanks to Eamon Daly for providing bug fixes for bugs in validate.js
+caused by HTML::Prototype.
 
 =head1 LICENSE
 
 This module may be distributed under the same terms as Perl itself.
 
-=cut
+=head1 AUTHOR
 
+Paul Seamons <paul at seamons dot com>
 
+=cut
This page took 0.082077 seconds and 4 git commands to generate.