The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Smolder::Constraints;
use strict;
use warnings;
use Smolder::DB;
use Smolder::DB::SmokeReport;
use Smolder::DB::Preference;
use Email::Valid;
use File::Basename;
use File::Temp;
use File::MMagic;
use File::Spec::Functions qw(catdir tmpdir);

our @ISA       = qw(Exporter);
our @EXPORT_OK = qw(
  email
  unsigned_int
  bool
  length_max
  length_min
  length_between
  enum_value
  unique_field_value
  existing_field_value
  file_mtype
  smoke_report_tags
);

=head1 NAME

Smolder::Constraints

=head1 SYNOPSIS

    use Smolder::Constraints qw(email unsigned_int max_length);

    # then in a D::FV profile
    my $form = {
        required    => [qw(email id last_name)],
        constraint_methods  => {
            email       => email(),
            id          => unsigned_int(),
            last_name   => max_length(255),
        }
    };

=head1 DESCRIPTION

This package provides/exports several routines that are useful
inside of Smolder for form validation, using L<Data::FormValidator>.
Each routine will return something suitable for use inside of
a C<constraint_methods> hash.

=head1 ROUTINES

=head2 email

Returns a method which validates an email address

=cut

sub email {
    return sub {
        my ($dfv, $value) = @_;
        if (Email::Valid->address($value)) {
            return $value;
        } else {
            return;
        }
    };
}

=head2 unsigned_int

Returns regex that assures the data is simply an unsigned integer

=cut

sub unsigned_int {
    return qr/^\d+$/;
}

=head2 bool

Returns a regex that assures the data is either a '1' or a '0'

=cut

sub bool {
    return qr/^1|0$/;
}

=head2 length_max

Given an integer $max, returns a regex that assures the data has 
at most $max number of printable characters.

=cut

sub length_max {
    my $max = shift;
    return qr/^[[:print:]\s]{1,$max}$/;
}

=head2 length_min

Given an integer $min, returns a regex that assures the data has
at least $min number of printable characters.

=cut

sub length_min {
    my $min = shift;
    return qr/^[[:print:]\s]{$min,}$/;
}

=head2 length_between

Given an integer $min and an integer $max, returns a regex that assures the data has
at least $min and at most $max number of printable characters.

=cut

sub length_between {
    my ($min, $max) = sort { $a <=> $b } @_;
    return qr/^[[:print:]]{$min,$max}$/;
}

=head2 enum_value

Returns a method which will make sure that the value is an allowable
enum value for the given table and column.

    enum_value('table', 'column');

=cut

sub enum_value {
    my ($table, $column) = @_;
    my $enums = Smolder::DB->enum_values($table, $column);
    return sub {
        my ($dfv, $value) = @_;
        foreach my $enum (@$enums) {
            if ($enum eq $value) {
                return $value;
            }
        }
        return;
    };
}

=head2 unique_field_value

Returns a method which will make sure that the value
being updated does not currently exist in the table and field
specified.  Can be passed an optional integer value which is 
used as the table's primary id to not compare against 
(this is useful when editing an existing row when you don't 
care it still has the same value or not).

    unique_field_value('project', 'name')

or

    unique_field_value('developer', 'username', 23)

=cut

sub unique_field_value {
    my ($table, $field, $id) = @_;

    return sub {
        my ($dfv, $value) = @_;
        $dfv->set_current_constraint_name("unique_${table}_${field}");

        # get all the values of a certain field
        my $sql = "SELECT $field FROM $table WHERE $field = ?";
        $sql .= " AND id != $id" if ($id);
        my $sth = Smolder::DB->db_Main->prepare_cached($sql);
        $sth->execute($value);
        my $row = $sth->fetchrow_arrayref();
        $sth->finish();
        if ($row) {
            return;
        } else {
            return $value;
        }
      }
}

=head2 existing_field_value 

Returns a sub that will verifiy that a value exists in a particular
table in a particular column.

    existing_field_value('developer', 'id')

=cut 

sub existing_field_value {
    my ($table, $column) = @_;
    return sub {
        my ($dfv, $value) = @_;
        my $sth = Smolder::DB->db_Main->prepare_cached(
            qq(
            SELECT $column FROM $table WHERE $column = ?
        )
        );
        $sth->execute($value);
        my $row = $sth->fetchrow_arrayref();
        $sth->finish();
        if (defined $row->[0]) {
            return $value;
        } else {
            return;
        }
      }
}

=head2 file_mtype

Returns a sub that will validate that the file is one of the given MIME types.
If it is valid, it will return the name of the temporary file currently
being used.

    file_mtype('text/plain', 'image/jpg'),

=cut

sub file_mtype {
    my @types = @_;
    return sub {
        my ($dfv, $filename) = @_;
        my $fh = $dfv->get_input_data()->upload($dfv->get_current_constraint_field);
        my ($suffix) = (basename($filename) =~ /(\..*)$/);

        # save the file to a temp location
        my $tmp = File::Temp->new(
            UNLINK => 0,
            SUFFIX => ($suffix || '.tmp'),
            DIR    => tmpdir(),
        ) or die "Could not create tmp file!";
        while (my $line = <$fh>) {
            print $tmp $line or die "Could not print to file '$tmp': $!";
        }
        close($tmp) or die "Could not close file '$tmp': $!";
        close($fh)  or die "Could not close upload FH: $!";

        # now get the file's mime-type
        my $mm   = File::MMagic->new();
        my $type = $mm->checktype_filename($tmp->filename);
        foreach my $t (@types) {
            if ($t eq $type) {
                return $tmp->filename;
            }
        }

        # if we got here then it wasn't valid, so remove the temp file
        unlink($tmp->filename) or die "Could not remove file '$tmp': $!";
        return;
      }
}

=head2 smoke_report_tags

Returns a sub that will verifiy that a value is a comma separated list
of tags that are no more than 255 characters each. If they are valid,
then an array ref of the tags will be returned.

    smoke_report_tags()

=cut 

sub smoke_report_tags {
    return sub {
        my ($dfv, $value) = @_;

        my @words = split(/\s*,\s*/, $value);
        foreach my $word (@words) {
            return if length $word > 255;
        }
        return \@words;
      }
}

1;