package DBIx::Class::Valiant;

use strict;
use warnings;

# Placeholder for now, not sure if there's going to be code here or not :)

1;

=head1 NAME

DBIx::Class::Valiant - Glue Valiant validations into DBIx::Class

=head1 SYNOPSIS

You need to add the components L<DBIx::Class::Valiant::Result> and L<DBIx::Class::Valiant::ResultSet>
to your result and result classes:

    package Example::Schema::Result::Person;

    use base 'DBIx::Class::Core';

    __PACKAGE__->load_components('Valiant::Result');

    package Example::Schema::ResultSet::Person;

    use base 'DBIx::Class::ResultSet';

    __PACKAGE__->load_components('Valiant::ResultSet');

Alternatively (and likely easier if you wish to use this across your entire DBIC schema) you can set these
on your base result / resultset classes:

    package Example::Schema::Result;

    use strict;
    use warnings;
    use base 'DBIx::Class';

    __PACKAGE__->load_components(qw/
      Valiant::Result
      Core
    /);

    package Example::Schema::ResultSet;

    use strict;
    use warnings;
    use base 'DBIx::Class::ResultSet';

    __PACKAGE__->load_components(qw/
      Valiant::ResultSet
    /);

There's an example schema in the C</example> directory of the distribution to give you
some hints.

B<NOTE> If you are using more than one component, you need to add these first. Hopefully
that is a restriction we can figure out how to remove (patches welcomed).

=head1 DESCRIPTION

B<NOTE>This works as in 'it passed my existing tests'.   Feel free to use it
if you are willing to get into the code, review / submit test cases, etc.   Also at some
point this will be pulled into its own distribution so please keep in mind.   I
will feel totally free to break backward compatibility on this until it seems
stable.  That being said support for validations at the single result level is pretty
firm and I can't imagine significant changes.  Support for validating nested results
(results that have has_many or similar relationships) is likely to evolve as bugs are
reported.

B<NOTE> Update: I consider this code to be beta stage and will now only break things if
its absolutely necessary to fix critical bugs or security matters.

This provides a base result component and resultset component that when added to your
classes glue L<Valiant> into L<DBIx::Class>.  You can set filters and validations on
your result source classes very similarily to how you would use L<Valiant> with L<Moo>
or L<Moose>.  Validations then run when you try to persist a change to the database; if
validations fail then we will not complete persisting the change (typically via insert
or update SQL).  Errors can be read via the C<errors> method just as on L<Moo> or L<Moose>
based validations.

Additionally we support nested creates and updates; validations follow any nested changes
and errors can be aggregated. Errors at any point in the nested create or update (or as
is often the cases a mixed situation) will cancel the entire changeset (issuing a rollback
if necessary).  Please note that as stated above nested support is still considered a beta
feature and bug reports / test cases (or patches) welcomed.

Documentation in this package only covers how L<Valiant> is glued into your result sources
and any local differences in behavior.   If you need a comprehensive overview of how
L<Valiant> works you should refer to that package.  Additionally if you are looking for how
to generate HTML forms you should refer to L<Valiant::HTML::Form>, L<Valiant::HTML::FormTags>
and L<Valiant::HTML::FormBuilder> (or refer to the example application under directory
'/example' for this distribution.)

=head2 Adding validations and filters

Assuming a base result and resultset as described above, you can add L<Valiant> style
validations to the result source columns or on the object as a whole.  The helpers described
in the L<Valiant> core documentations all work the same on your DBIC result source fields; the
main difference is that you must call these helpers as class/package methods:

    package Example::Schema::Result::Profile;

    use warnings;
    use strict;
    use base 'Example::Schema::Result';

    __PACKAGE__->table("profile");
    __PACKAGE__->add_columns(
      id => { data_type => 'bigint', is_nullable => 0, is_auto_increment => 1 },
      person_id => { data_type => 'integer', is_nullable => 0, is_foreign_key => 1 },
      state_id => { data_type => 'integer', is_nullable => 0, is_foreign_key => 1 },
      address => { data_type => 'varchar', is_nullable => 0, size => 48 },
      city => { data_type => 'varchar', is_nullable => 0, size => 32 },
      zip => { data_type => 'varchar', is_nullable => 0, size => 5 },
      birthday => { data_type => 'date', is_nullable => 1, datetime_undef_if_invalid => 1 },
      phone_number => { data_type => 'varchar', is_nullable => 1, size => 32 },
    );

    __PACKAGE__->set_primary_key("id");
    __PACKAGE__->add_unique_constraint(['id','person_id']);

    __PACKAGE__->belongs_to(
      state =>
      'Example::Schema::Result::State',
      { 'foreign.id' => 'self.state_id' }
    );

    __PACKAGE__->belongs_to(
      person =>
      'Example::Schema::Result::Person',
      { 'foreign.id' => 'self.person_id' }
    );

    __PACKAGE__->validates(address => (presence=>1, length=>[2,48]));
    __PACKAGE__->validates(city => (presence=>1, length=>[2,32]));
    __PACKAGE__->validates(zip => (presence=>1, format=>'zip'));
    __PACKAGE__->validates(phone_number => (presence=>1, length=>[10,32]));
    __PACKAGE__->validates(state_id => (presence=>1));
    __PACKAGE__->validates(birthday => (
        date => {
          max => sub { pop->now->subtract(days=>1) }, # can't be born yesterday
          min => sub { pop->years_ago(30) }, # Don't trust anyone over 30
        }
      )
    );

    1;

Besides the requirement of calling C<validates>, C<validates_with> and C<filters> as class
methods there are a few other changes from the core L<Valiant> behavior.

First C<validate> is automatically run for you when you attempt to C<create> or C<insert>
a record (or use related methods such as C<find_or_create>).  If validation fails the database
operation will not occur but you will get a DBIC result with the records in cache along
with any validation results.  This makes it easier to round trip things like form validation
since you can use the database result in your form response logic.  For example:

    my $person = $schema->resultset('Person')->first;
    my $state = $schema->resultset('State')->first;

    my $profile = $person->create_related('profile', +{
        zip => "78621",
        city => 'E',
        address => '15604 Harry Lind Road',
        birthday => DateTime->now->subtract(years=>55)->ymd,
        phone_number => '2123879509',
        state_id => $state->id,
    });

    $profile->invalid; # TRUE, so the record was NOT created!

But the record does have the attempted values cached as expected, even if its not in
the storage DB:

    print $profile->zip;      # 78621 
    print $profile->city;     # E

And you can query for errors:
    
    Dumper $person_profile->errors->to_hash(full_messages=>1);

Returns:

    city => [
      "city is too short (minimum is 2 characters)",
    ],
    birthday => [
      "Birthday chosen date can't be before @{[ DateTime->now->subtract(years=>30)->ymd ]}",
    ],

It might seem strange to have a C<$profile> object with invalid data, but it greatly simplifies
your UI layer since you can use the same model for both valid and invalid data (and to query for
errors.

=head2 validation context

By default when creating a new record we add a validation context 'create', and for updating we
add a 'update' context.  This way you can specify different validation rules for create and update
very easily.  If you want to add a custom context youc an do so with the C<context> method which
is part of your result API.

    $person->context('registration')->update(\%records);

Or you can add the context directly to your C<update> or C<create> call:

    $person->update(+{ __context=>'registration', %records});

See the core L<Valiant> documentation for more on validation contexts.

=head2 Combining validations into column definitions

If you are hand writing your table source definitions you can add validations directly
onto a column definition.   You might prefer this if you think it looks neater and adds
fewer lines of code.

    package Example::Schema::Result::CreditCard;

    use strict;
    use warnings;

    use base 'Example::Schema::Result';

    __PACKAGE__->table("credit_card");
    __PACKAGE__->load_components(qw/Valiant::Result/);
    __PACKAGE__->set_primary_key("id");

    __PACKAGE__->add_columns(
      id => { data_type => 'integer', is_nullable => 0, is_auto_increment => 1 },
      person_id => { data_type => 'integer', is_nullable => 0, is_foreign_key => 1 },
      card_number => { 
        data_type => 'varchar', 
        is_nullable => 0, 
        size => '20',
        validates => [ presence=>1, length=>[13,20], with=>'looks_like_a_cc' ],
        filters => [trim => 1],
      },
      expiration => { 
        data_type => 'date', 
        is_nullable => 0,
        validates => [ presence=>1, with=>'looks_like_a_datetime', with=>'is_future' ],
      },
    );

As in the example above you can define filters this way as well.

=head2 DBIC Candy

This has L<DBIx::Class::Candy> integration if you use that and prefer it:

    package Schema::Create::Result::Person;

    use DBIx::Class::Candy -base => 'Schema::Result';

    table "person";

    column id => { data_type => 'bigint', is_nullable => 0, is_auto_increment => 1 };
    column username => { data_type => 'varchar', is_nullable => 0, size => 48 };
    column first_name => { data_type => 'varchar', is_nullable => 0, size => 24 };
    column last_name => { data_type => 'varchar', is_nullable => 0, size => 48 };
    column password => { data_type => 'varchar', is_nullable => 0, size => 64 };

    primary_key "id";
    unique_constraint ['username'];

    might_have profile => (
      'Schema::Create::Result::Profile',
      { 'foreign.person_id' => 'self.id' }
    );

    filters username => (trim => 1);

    validates profile => (result=>+{validations=>1} );
    validates username => (presence=>1, length=>[3,24], format=>'alpha_numeric', unique=>1);
    validates first_name => (presence=>1, length=>[2,24]);
    validates last_name => (presence=>1, length=>[2,48]);
    validates password => (presence=>1, length=>[8,24]);
    validates password => (confirmation => { on=>'create' } );
    validates password => (confirmation => { 
        on => 'update',
        if => 'is_column_changed', # This method defined by DBIx::Class::Row
      });
     
    accept_nested_for 'profile', {update_only=>1};

    1;

When using this with L<DBIx::Class::Candy> the following class methods are available as exports:

  'filters', 'validates', 'filters_with', 'validates_with', 'accept_nested_for',
  'auto_validation'

=head1 EXAMPLE

Assuming you have a base result class C<Example::Schema::Result> which uses the L<DBIx::Class::Valiant::Result>
component and a default resultset class which uses the L<Example::Schema::ResultSet> component:

    package Example::Schema::Result::Person;

    use warnings;
    use strict;
    use base 'Example::Schema::Result';

    __PACKAGE__->table("person");

    __PACKAGE__->add_columns(
      id => { data_type => 'bigint', is_nullable => 0, is_auto_increment => 1 },
      username => { data_type => 'varchar', is_nullable => 0, size => 48 },
      first_name => { data_type => 'varchar', is_nullable => 0, size => 24 },
      last_name => { data_type => 'varchar', is_nullable => 0, size => 48 },
    );

    __PACKAGE__->validates(username => presence=>1, length=>[3,24], format=>'alpha_numeric', unique=>1);
    __PACKAGE__->validates(first_name => (presence=>1, length=>[2,24]));
    __PACKAGE__->validates(last_name => (presence=>1, length=>[2,48]));

We now have L<Valiant> validations wrapping any method which mutates the database.

    my $person = $schema->resultset('Person')
      ->create({
        username => 'jjn',   # for this example we'll say this username is taken in the DB
        first_name => 'j',   # too short
        last_name => 'n',    # too short
      });

In this case since we tried to create a record that is not valid the create will be aborted (not saved
to the DB and an object will be returned with errors:

    $person->invalid; # true;
    $person->errors->size; # 3
    person->errors->full_messages_for('username');  # ['Username is not unique']
    person->errors->full_messages_for('first_name');  # ['First Name is too short']
    person->errors->full_messages_for('last_name');  # ['Last Name is too short']

The object will have the invalid values properly populated:

    $person->get_column('username');  # jjn

You might find this useful for building error message responses in for example html forms or other types
or error responses.

See C<example> directory for web application using L<Catalyst> that uses this for more details.

=head1 NESTED VALIDATIONS

With a properly setup schema you can propagate creates/updates down into related result sources.  Because
this can be a security issue you must configure your result source classes to allow it.  For example, let's
say you have a C<Person> result source that has a C<Profile> (via 'might_have'), some C<Roles> via a 
'has_many' bridge result class called C<PersonRoles> and finally a list of C<CreditCards> directly via
a 'has_many'.  For the purposes of this example assume the base result and result set classes are setup
to consume the Valiant components as in the L</SYNOPSIS>.

    package Example::Schema::Result::Person;

    use warnings;
    use strict;
    use base 'Example::Schema::Result';

    # This first part is just the normal DBIC class data you assign to a result class
    # in order to read and update tables as well as follow relationships:

    __PACKAGE__->table("person");

    __PACKAGE__->add_columns(
      id => { data_type => 'bigint', is_nullable => 0, is_auto_increment => 1 },
      username => { data_type => 'varchar', is_nullable => 0, size => 48 },
      first_name => { data_type => 'varchar', is_nullable => 0, size => 24 },
      last_name => { data_type => 'varchar', is_nullable => 0, size => 48 },
    );

    __PACKAGE__->set_primary_key("id");
    __PACKAGE__->add_unique_constraint(['username']);

    __PACKAGE__->might_have(
      profile =>
      'Example::Schema::Result::Profile',
      { 'foreign.person_id' => 'self.id' }
    );

    __PACKAGE__->has_many(
      credit_cards =>
      'Example::Schema::Result::CreditCard',
      { 'foreign.person_id' => 'self.id' }
    );

    __PACKAGE__->has_many(
      person_roles =>
      'Example::Schema::Result::PersonRole',
      { 'foreign.person_id' => 'self.id' }
    );

    __PACKAGE__->many_to_many('roles' => 'person_roles', 'role');

    # In this next part we annotate this class with additional meta data which Valiant will
    # use to performance validations as well as allow you to follow relationships at create / update
    # time:

    __PACKAGE__->validates(username => presence=>1, length=>[3,24], format=>'alpha_numeric', unique=>1);
    __PACKAGE__->validates(first_name => (presence=>1, length=>[2,24]));
    __PACKAGE__->validates(last_name => (presence=>1, length=>[2,48]));

    __PACKAGE__->validates(credit_cards => (set_size=>+{min=>2, max=>4}));
    __PACKAGE__->accept_nested_for('credit_cards', +{allow_destroy=>1});

    __PACKAGE__->validates(person_roles => (set_size=>+{min=>1}));
    __PACKAGE__->accept_nested_for('person_roles', {allow_destroy=>1});

    __PACKAGE__->validates(profile => (presence=>1));
    __PACKAGE__->accept_nested_for('profile');

So in brief we add some simple validations on fields in the C<Person> result class to validate
things like the length of text fields and we added things like requiring a person to have at least
one C<person_role> and two C<credit_cards>; we also specify that we want to follow those nested
relationships and aggregate errors into the C<Persons> result class (we'll see the validation
definitions for those classes below).   Also we allow that we can delete C<credit_cards> as well
as C<person_roles>.

Here's how we setup these related classes.  Please note that we in general avoid using the 'many_to_many'
pseudo relationship.  DBIC does not define enough internal meta data for m2m to work reliably and we had
other issues around caching.  As a result I recommend avoiding m2m for using with L<Valiant> although it
seems to work ok for simple use cases.  Test cases and patches to improve this are welcomed (you can see
there's quite a few m2m test cases already but I was never able to get the type of consistency I felt 
needed for a data storage layer where integrity is most important).

    package Example::Schema::Result::Profile;

    use warnings;
    use strict;
    use base 'Example::Schema::Result';

    __PACKAGE__->table("profile");
    __PACKAGE__->load_components(qw/Valiant::Result/);

    __PACKAGE__->add_columns(
      id => { data_type => 'bigint', is_nullable => 0, is_auto_increment => 1 },
      person_id => { data_type => 'integer', is_nullable => 0, is_foreign_key => 1 },
      address => { data_type => 'varchar', is_nullable => 0, size => 48 },
      city => { data_type => 'varchar', is_nullable => 0, size => 32 },
      zip => { data_type => 'varchar', is_nullable => 0, size => 5 },
      birthday => { data_type => 'date', is_nullable => 1 },
      phone_number => { data_type => 'varchar', is_nullable => 1, size => 32 },
    );

    __PACKAGE__->validates(address => (presence=>1, length=>[2,48]));
    __PACKAGE__->validates(city => (presence=>1, length=>[2,32]));
    __PACKAGE__->validates(zip => (presence=>1, format=>'zip'));
    __PACKAGE__->validates(phone_number => (presence=>1, length=>[10,32]));
    __PACKAGE__->validates(birthday => (
        date => {
          max => sub { pop->now->subtract(days=>1) }, # Can't be born yesterday :)
          min => sub { pop->years_ago(30) }, # Don't trust anyone over 30
        }
      )
    );

    __PACKAGE__->set_primary_key("id");
    __PACKAGE__->add_unique_constraint(['id','person_id']);

    __PACKAGE__->belongs_to(
      person =>
      'Example::Schema::Result::Person',
      { 'foreign.id' => 'self.person_id' }
    );

    package Example::Schema::Result::CreditCard;

    use strict;
    use warnings;
    use DateTime;
    use DateTime::Format::Strptime;

    use base 'Example::Schema::Result';

    __PACKAGE__->table("credit_card");
    __PACKAGE__->load_components(qw/Valiant::Result/);

    __PACKAGE__->add_columns(
      id => { data_type => 'integer', is_nullable => 0, is_auto_increment => 1 },
      person_id => { data_type => 'integer', is_nullable => 0, is_foreign_key => 1 },
      card_number => { data_type => 'varchar', is_nullable => 0, size => '20' },
      expiration => { data_type => 'date', is_nullable => 0 },
    );

    __PACKAGE__->set_primary_key("id");

    __PACKAGE__->validates(card_number => (presence=>1, length=>[13,20], with=>'looks_like_a_cc' ));
    __PACKAGE__->validates(expiration => (presence=>1, with=>'looks_like_a_datetime', with=>'is_future' ));

    __PACKAGE__->belongs_to(
      person =>
      'Example::Schema::Result::Person',
      { 'foreign.id' => 'self.person_id' }
    );

    sub looks_like_a_cc {
      my ($self, $attribute_name, $value) = @_;
      return if $value =~/^\d{13,20}$/;
      $self->errors->add($attribute_name, 'does not look like a credit card'); 
    }

    my $strp = DateTime::Format::Strptime->new(pattern => '%Y-%m-%d');

    sub looks_like_a_datetime {
      my ($self, $attribute_name, $value) = @_;
      my $dt = $strp->parse_datetime($value);
      $self->errors->add($attribute_name, 'does not look like a datetime value') unless $dt;
    }

    sub is_future {
      my ($self, $attribute_name, $value) = @_;
      my $dt = $strp->parse_datetime($value);
      return unless $dt;  # Skip this validation if the user didn't give us a date time
                          # format (that's caught by ->looks_like_a_datetime).
      $self->errors->add($attribute_name, 'must be in the future') unless $dt > DateTime->now);
    }

    package Example::Schema::Result::PersonRole;

    use strict;
    use warnings;
    use base 'Example::Schema::Result';

    __PACKAGE__->table("person_role");
    __PACKAGE__->load_components(qw/Valiant::Result/);

    __PACKAGE__->add_columns(
      person_id => { data_type => 'integer', is_nullable => 0, is_foreign_key => 1 },
      role_id => { data_type => 'integer', is_nullable => 0, is_foreign_key => 1 },
    );

    __PACKAGE__->set_primary_key("person_id", "role_id");

    __PACKAGE__->belongs_to(
      person =>
      'Example::Schema::Result::Person',
      { 'foreign.id' => 'self.person_id' }
    );

    __PACKAGE__->belongs_to(
      role =>
      'Example::Schema::Result::Role',
      { 'foreign.id' => 'self.role_id' }
    );

    package Example::Schema::Result::Role;

    use strict;
    use warnings;

    use base 'Example::Schema::Result';

    __PACKAGE__->table("role");
    __PACKAGE__->load_components(qw/Valiant::Result/);

    __PACKAGE__->add_columns(
      id => { data_type => 'integer', is_nullable => 0, is_auto_increment => 1 },
      label => { data_type => 'varchar', is_nullable => 0, size => '24' },
    );

    __PACKAGE__->set_primary_key("id");
    __PACKAGE__->add_unique_constraint(['label']);

    __PACKAGE__->has_many(
      person_roles =>
      'Example::Schema::Result::PersonRole',
      { 'foreign.role_id' => 'self.id' }
    );

With this setup you could deeply validate / create a Person and its permitted relationships:

    my $person = $schema
      ->resultset('Person')
      ->create({
        first_name => "john",
        last_name => "nap",
        username => "jjn1",
        profile => {
          address => "15604 Harry Lind Road",
          birthday => "2000-02-13",
          city => "Elgin",
          phone_number => "16467081837",
          zip => '10000'
        },
        person_roles => [   
          { role_id => 1 },
          { role_id => 2 },
          { role_id => 4 },
        ],
        credit_cards => [
          {
            card_number => "3423423423423423",
            expiration => "2222-02-02",
          },
          {
            card_number => "1111222233334444",
            expiration => "2333-02-02",
          },
        ],
      });

If the proposed data fails validation then you won't create any records, but the errors can
be viewed via the C<errors> collection.

For doing validation on nested updates you need to do a bit more work to make sure you caches
are properly populated from the database. You need to use 'prefetch' to locally cache all the
results you are trying to validate:

    my $person = Schema->resultset('Person')->find(
      { 'me.id'=>$pid },
      { prefetch => ['profile', 'credit_cards', {person_roles => 'role' }] }
    );
    $person->build_related_if_empty('profile');

Then you can do an update:

    $person->update({
      username => 'jjn2',
      profile => {
        city => 'NYC'
      },
    });

As before if there's a valiation issue the update won't happen.

B<NOTE> Recursion warning!  You cannot currently C<accept_nested_for> for a relationship
whose target result source is setting C<accept_nested_for> into yourself.   This is probably
fixable, patches and use cases welcomed!

=head2 Deleting

If a nested relationship permits deleting (via the 'allow_destroy' flag) you can mark a row for deletion
directly using the C<_delete> flag:

    $person->update({
      username => 'jjn2',
        person_roles => [   
          { role_id => 1 },
          { role_id => 2, _delete => 1 },
          { role_id => 4, _delete => 1 },
        ],
    });

B<Inplicit Deletion>: When deletion is permitted we automatically mark nested object that are fetched from
'prefetch' as deleted if they do not appear in the update statement.  B<NOTE>: This behavior is still
under review and might change in the future so if you rely on it please be sure to follow update notes
on newer versions of this code.

This example is far from complete, for now see example in C<example> directory of the distribution
and more examples in the tests directory.  In particular this example doesn't really cover all the
ins and outs of deleting.  An overall tutorial is in the works but example submission or questions
(that could eventually lead to a FAQ) are very welcomed.

=head1 WARNINGS & GOTCHAs

You must be careful with how you expose a deeply nested interface.   If you simply pass fields 
from a web form you are potentially opening yourself to SQL injection and similar types of attacks.  I 
recommend being very careful to sanitize incoming parameters, especially any related keys.

B<NOTE>Be careful when reusing a result that has deeply nested values after you create or update.
In order to keep data consistent, DBIC will blow away any prefetch caches after create or update.
So that means if you try to reuse a result you may encounter errors that cause following
creates or inserts to fails, especially if you have constraints on the related data, such as SetSize.

=head2 Many to Many

Many to Many type relationships are supported to the best of my ability but since these types of
fake relationships have lots of known issues you are more likely to run into edge cases.  In
particular its a bad idea to have validations on both a m2m relations and also on the one to many
relation that it bridges.   This is likely to result in false positive validation errors due to the
way the resultset cache works (and doesn't work) for m2m.

Happy to take patches for improvements to anyone that feels strongly about it.  FWIW the core DBIC
development team recommends staying away fron using the many to many code.

=head1 SEE ALSO
 
L<Valiant>, L<DBIx::Class::Valiant::Result>, L<DBIx::Class::Valiant::ResultSet>, L<DBIx::Class>

=head1 AUTHOR
 
See L<Valiant>

=head1 COPYRIGHT & LICENSE
 
See L<Valiant>

=cut