Anansi-DatabaseComponent

 view release on metacpan or  search on metacpan

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

=head1 SYNOPSIS

    package Anansi::Database::Example;

    use base qw(Anansi::DatabaseComponent);

    sub connect {
        my ($self, $channel, %parameters) = @_;
        return $self->SUPER::connect(
            undef,
            INPUT => [
                'some text',
                {
                    NAME => 'someParameter',
                }, {
                    INPUT => [
                        'more text',
                        {
                            NAME => 'anotherParameter',
                        },
                        'yet more text',
                    ]
                }, {
                    DEFAULT => 'abc',
                    NAME => 'yetAnotherParameter',
                },

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

            undef,
            $component,
            someParameter => 'some data',
            anotherParameter => 'more data',
            yetAnotherParameter => 'further data',
        )) {
            my $result = $database->statement(
                undef,
                $component,
                SQL => 'SELECT someThing FROM someTable where modified = ?;',
                INPUT => [
                    {
                        NAME => 'modified',
                    },
                ],
                modified => '2011-02-22 00:21:46',
            );
            if(!defined($result)) {
            } elsif(ref($result) =~ /^ARRAY$/i) {
                foreach my $record (@{$result}) {
                    next if(ref($record) !~ /^HASH$/i);

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

    return 0;
}

Anansi::DatabaseComponent::addChannel('Anansi::DatabaseComponent', 'AUTOCOMMIT' => 'autocommit');


=head2 bind

    if(Anansi::DatabaseComponent::bind($OBJECT,
        HANDLE => $HANDLE,
        INPUT => [
            {
                NAME => 'someParameter'
            }, {
                DEFAULT => 123,
                NAME => 'anotherParameter'
            }
        ],
        VALUE => {
            someParameter => 'abc'
        }
    ));

    if($OBJECT->bind(
        HANDLE => $HANDLE,
        INPUT => [
            {
                NAME => 'yetAnotherParameter',
                TYPE => 'TEXT'
            }
        ],
        VALUE => [
            yetAnotherParameter => 456
        ]
    ));

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

=item parameters I<(Hash, Optional)>

Named parameters.

=over 4

=item HANDLE I<(DBI::st, Required)>

The database statement handle.

=item INPUT I<(Array, Required)>

An array of hashes.  Each element of the array corresponds to an equivalent B<?>
I<(Question mark)> within the prepared SQL statement.  Each hash contains a
I<NAME> key with a value that represents a possible key within the I<VALUE>
parameter.  Each hash may also contain a I<DEFAULT> key which contains the value
to use if the equivalent I<VALUE> parameter does not exist and a I<TYPE> key
which contains the SQL type to associate with the assigned value.  When no
corresponding I<VALUE> parameter key exists and no I<DEFAULT> key has been
defined then an empty string is used for the value.

=item VALUE I<(Hash, Required)>

A hash of values to assign in the order specified by the I<INPUT> parameter.

=back

=back

Attempts to use the supplied parameters to assign values to a SQL statement that
has already been prepared to accept them.  Returns B<0> I<(zero)> on failure and
the database statement handle on success.

=cut


sub bind {
    my ($self, %parameters) = @_;
    return 0 if(!defined($parameters{HANDLE}));
    return 0 if(!defined($parameters{INPUT}));
    return 0 if(ref($parameters{INPUT}) !~ /^ARRAY$/i);
    return 0 if(!defined($parameters{VALUE}));
    return 0 if(ref($parameters{VALUE}) !~ /^HASH$/i);
    my $index = 1;
    foreach my $input (@{$parameters{INPUT}}) {
        if(defined(${$parameters{VALUE}}{${$input}{NAME}})) {
            if(defined(${$input}{TYPE})) {
                $parameters{HANDLE}->bind_param($index, ${$parameters{VALUE}}{${$input}{NAME}}, ${$input}{TYPE});
            } else {
                $parameters{HANDLE}->bind_param($index, ${$parameters{VALUE}}{${$input}{NAME}});
            }
        } elsif(defined(${$input}{DEFAULT})) {
            if(defined(${$input}{TYPE})) {
                $parameters{HANDLE}->bind_param($index, ${$input}{DEFAULT}, ${$input}{TYPE});
            } else {

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN


=item parameters I<(Array, Optional)>

An array of hashes.  Each hash should contain a I<NAME> key with a string value.

=back

Verifies that the supplied parameters are all hashes and that they each contain
a I<NAME> key with a string value.  Returns B<1> I<(one)> when validity is
confirmed and B<0> I<(zero)> when an invalid structure is determined.  Used to
validate the I<INPUT> parameter of the B<bind> method.

=cut


sub binding {
    my ($self, @parameters) = @_;
    foreach my $parameter (@parameters) {
        return 0 if(ref($parameter) !~ /^HASH$/i);
        return 0 if(!defined(${$parameter}{NAME}));
        return 0 if(ref(${$parameter}{NAME}) !~ /^$/);

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

    return 1 if($commit);
    return 0;
}

Anansi::DatabaseComponent::addChannel('Anansi::DatabaseComponent', 'COMMIT' => 'commit');


=head2 connect

    if(1 == Anansi::DatabaseComponent::connect($OBJECT, undef
        INPUT => [
            'some text',
            {
                NAME => 'someParameter'
            }, {
                INPUT => [
                    'more text',
                    {
                        NAME => 'anotherParameter'
                    },
                    'yet more text'
                ]
            }, {
                DEFAULT => 'abc',
                NAME => 'yetAnotherParameter'
            }
        ],
        someParameter => 12345,
        anotherParameter => 'blah blah blah'
    ));

    if(1 == Anansi::DatabaseComponent::channel($OBJECT, 'CONNECT',
        INPUT => [
            'blah blah blah',
            {
                DEFAULT => 123,
                NAME => 'someParameter',
            }
        ],
        someParameter => 'some text'
    ));

    if(1 == $OBJECT->connect(undef,
        INPUT => [
            {
                INPUT => [
                    'some text',
                    {
                        NAME => 'someParameter'
                    },
                    'more text'
                ]
            }
        ],
        someParameter => 'in between'
    ));

    if(1 == $OBJECT->channel('CONNECT',
        INPUT => [
            {
                INPUT => [
                    {
                        NAME => 'abc'
                    }, {
                        NAME => 'def'
                    }
                },
                REF => 'HASH'
            }
        ]
    ));

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN


=item parameters I<(Hash, Required)>

Named parameters.

=over 4

=item HANDLE I<(DBI::db, Optional)>

The database handle of an existing database connection.  Used in preference to
the I<INPUT> parameter.

=item INPUT I<(Array B<or> Scalar, Optional)>

An array or single value containing a description of each parameter in the order
that it is passed to the database driver's I<connect> method.  Used when the
I<HANDLE> parameter does not exist.

=over 4

=item I<(Non-Hash)>

An element that does not contain a hash value will be used as the corresponding

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

An element that contains a hash value is assumed to be a description of how to
generate the corresponding I<connect> method's parameter value.  when a value
can not be generated, an B<undef> value will be used.

=over 4

=item DEFAULT I<(Optional)>

The value to use if no other value can be determined.

=item INPUT I<(Array B<or> Scalar, Optional)>

Contains a structure like that given in I<INPUT> above with the exception that
any further I<INPUT> keys will be ignored.  As this key is only valid when
I<NAME> is undefined and I<REF> either specifies a string or a hash, it's value
will be either a concatenation of all the calculated strings or a hash
containing all of the specified keys and values.

=item NAME I<(String, Optional)>

The name of the parameter that contains the value to use.

=item REF I<(Array B<or> String, Optional)>

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN



sub connect {
    my ($self, $channel, %parameters) = @_;
    return 0 if(ref($self) =~ /^(|ARRAY|CODE|FORMAT|GLOB|HASH|IO|LVALUE|REF|Regexp|SCALAR|VSTRING)$/i);
    $self->disconnect();
    if(defined($parameters{HANDLE})) {
        return 0 if(ref($parameters{HANDLE}) !~ /^DBI::db$/);
        $self->{HANDLE} = $parameters{HANDLE};
        $self->{MANAGE_HANDLE} = 0;
    } elsif(!defined($parameters{INPUT})) {
        return 0;
    } elsif(ref($parameters{INPUT}) !~ /^ARRAY$/i) {
        return 0;
    } else {
        my @inputs;
        foreach my $input (@{$parameters{INPUT}}) {
            if(ref($input) !~ /^HASH$/i) {
                push(@inputs, $input);
                next;
            }
            my $value = undef;
            $value = ${$input}{DEFAULT} if(defined(${$input}{DEFAULT}));
            if(!defined(${$input}{NAME})) {
                if(!defined(${$input}{INPUT})) {
                } elsif(ref(${$input}{INPUT}) !~ /^ARRAY$/i) {
                } elsif(!defined(${$input}{REF})) {
                } elsif(ref(${$input}{REF}) !~ /^$/i) {
                } elsif('' eq ${$input}{REF}) {
                    my @subInputs;
                    for(my $index = 0; $index < scalar(@{${$input}{INPUT}}); $index++) {
                        if(ref(${${$input}{INPUT}}[$index]) =~ /^$/i) {
                            push(@subInputs, ${${$input}{INPUT}}[$index]);
                            next;
                        } elsif(ref(${${$input}{INPUT}}[$index]) !~ /^HASH$/) {
                            next;
                        }
                        my $subValue = '';
                        $subValue = ${${${$input}{INPUT}}[$index]}{DEFAULT} if(defined(${${${$input}{INPUT}}[$index]}{DEFAULT}));
                        if(!defined(${${${$input}{INPUT}}[$index]}{NAME})) {
                        } elsif(ref(${${${$input}{INPUT}}[$index]}{NAME}) !~ /^$/) {
                        } elsif(defined($parameters{${${${$input}{INPUT}}[$index]}{NAME}})) {
                            if(!defined(${${${$input}{INPUT}}[$index]}{REF})) {
                                $subValue = $parameters{${${${$input}{INPUT}}[$index]}{NAME}} if('' eq ref($parameters{${${${$input}{INPUT}}[$index]}{NAME}}));
                            } elsif(ref(${${${$input}{INPUT}}[$index]}{REF}) !~ /^$/) {
                            } elsif('' ne ${${${$input}{INPUT}}[$index]}{REF}) {
                            } elsif('' ne ref($parameters{${${${$input}{INPUT}}[$index]}{NAME}})) {
                            } else {
                                $subValue = $parameters{${${${$input}{INPUT}}[$index]}{NAME}};
                            }
                        }
                        push(@subInputs, $subValue);
                    }
                    $value = join('', @subInputs);
                } elsif(${$input}{REF} =~ /^HASH$/i) {
                    my %subInputs;
                    foreach my $subInput (@{${$input}{INPUT}}) {
                        next if(ref($subInput) !~ /^HASH$/i);
                        my $subValue = undef;
                        $subValue = ${$subInput}{DEFAULT} if(defined(${$subInput}{DEFAULT}));
                        if(!defined(${$subInput}{NAME})) {
                        } elsif(ref(${$subInput}{NAME}) !~ /^$/) {
                        } elsif(defined($parameters{${$subInput}{NAME}})) {
                            if(!defined(${$subInput}{REF})) {
                            } elsif(ref(${$subInput}{REF}) =~ /^ARRAY$/i) {
                                my %refs = map { $_ => 1 } (@{${$subInput}{REF}});
                                $subValue = $parameters{${$subInput}{NAME}} if(defined($refs{ref($parameters{${$subInput}{NAME}})}));

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

                }
            } elsif(ref(${$input}{NAME}) !~ /^$/) {
            } elsif(defined($parameters{${$input}{NAME}})) {
                if(!defined(${$input}{REF})) {
                } elsif(ref(${$input}{REF}) =~ /^ARRAY$/i) {
                    my %refs = map { $_ => 1 } (@{${$input}{REF}});
                    if(!defined($refs{ref($parameters{${$input}{NAME}})})) {
                    } elsif(ref($parameters{${$input}{NAME}}) !~ /^HASH$/i) {
                        $value = $parameters{${$input}{NAME}};
                    } else {
                        if(!defined(${$input}{INPUT})) {
                            $value = $parameters{${$input}{NAME}};
                        } elsif(ref(${$input}{INPUT}) !~ /^HASH$/i) {
                            $value = $parameters{${$input}{NAME}};
                        } else {
                            my %subInputs;
                            foreach my $subInput (keys(%{${$input}{INPUT}})) {
                                if(ref($subInput) !~ /^HASH$/i) {
                                    $subInputs{$subInput} = $subInput;
                                    next;
                                }
                                my $subValue = undef;
                                $value = ${${${$input}{INPUT}}{$subInput}}{DEFAULT} if(defined(${${${$input}{INPUT}}{$subInput}}{DEFAULT}));
                                if(!defined(${${${$input}{INPUT}}{$subInput}}{NAME})) {
                                } elsif(ref(${${${$input}{INPUT}}{$subInput}}{NAME}) !~ /^$/) {
                                } elsif(defined($parameters{${${${$input}{INPUT}}{$subInput}}{NAME}})) {
                                    if(!defined(${${${$input}{INPUT}}{$subInput}}{REF})) {
                                    } elsif(ref(${${${$input}{INPUT}}{$subInput}}{REF}) =~ /^ARRAY$/i) {
                                        my %refs = map { $_ => 1 } (@{${${${$input}{INPUT}}{$subInput}}{REF}});
                                        $subValue = $parameters{${${${$input}{INPUT}}{$subInput}}{NAME}} if(defined($refs{ref($parameters{${${${$input}{INPUT}}{$subInput}}{NAME}})}));
                                    } elsif(ref(${${${$input}{INPUT}}{$subInput}}{REF}) !~ /^$/) {
                                    } elsif(${${${$input}{INPUT}}{$subInput}}{REF} ne ref($parameters{${${${$input}{INPUT}}{$subInput}}{NAME}})) {
                                    } else {
                                        $subValue = $parameters{${${${$input}{INPUT}}{$subInput}}{NAME}};
                                    }
                                }
                                $subInputs{$subInput} = $subValue;
                            }
                            $value = \%subInputs;
                        }
                    }
                } elsif(ref(${$input}{REF}) !~ /^$/) {
                } elsif(${$input}{REF} ne ref($parameters{${$input}{NAME}})) {
                } elsif(ref($parameters{${$input}{NAME}}) !~ /^HASH$/i) {
                    $value = $parameters{${$input}{NAME}};
                } else {
                    if(!defined(${$input}{INPUT})) {
                        $value = $parameters{${$input}{NAME}};
                    } elsif(ref(${$input}{INPUT}) !~ /^HASH$/i) {
                        $value = $parameters{${$input}{NAME}};
                    } else {
                        my %subInputs;
                        foreach my $key (keys(%{${$input}{INPUT}})) {
                            if(ref($subInput) !~ /^HASH$/i) {
                                push(@subInputs, $subInput);
                                next;
                            }
                            my $subValue = undef;
                            $value = ${${${$input}{INPUT}}{$subInput}}{DEFAULT} if(defined(${${${$input}{INPUT}}{$subInput}}{DEFAULT}));
                            if(!defined(${${${$input}{INPUT}}{$subInput}}{NAME})) {
                            } elsif(ref(${${${$input}{INPUT}}{$subInput}}{NAME}) !~ /^$/) {
                            } elsif(defined($parameters{${${${$input}{INPUT}}{$subInput}}{NAME}})) {
                                if(!defined(${${${$input}{INPUT}}{$subInput}}{REF})) {
                                } elsif(ref(${${${$input}{INPUT}}{$subInput}}{REF}) =~ /^ARRAY$/i) {
                                    my %refs = map { $_ => 1 } (@{${${${$input}{INPUT}}{$subInput}}{REF}});
                                    $subValue = $parameters{${${${$input}{INPUT}}{$subInput}}{NAME}} if(defined($refs{ref($parameters{${${${$input}{INPUT}}{$subInput}}{NAME}})}));
                                } elsif(ref(${${${$input}{INPUT}}{$subInput}}{REF}) !~ /^$/) {
                                } elsif(${${${$input}{INPUT}}{$subInput}}{REF} ne ref($parameters{${${${$input}{INPUT}}{$subInput}}{NAME}})) {
                                } else {
                                    $subValue = $parameters{${${${$input}{INPUT}}{$subInput}}{NAME}};
                                }
                            }
                            $subInputs{$subInput} = $subValue;
                        }
                        $value = \%subInputs;
                    }
                }
            }
            push(@inputs, $value);
        }

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN



=head2 prepare

    my $PREPARATION = if(1 == Anansi::DatabaseComponent::prepare($OBJECT, undef,
        STATEMENT => 'an associated name'
    );
    if(defined($PREPARATION));

    if(1 == Anansi::DatabaseComponent::channel($OBJECT, 'PREPARE',
        INPUT => [
            {
                NAME => 'someParameter'
            }
        ],
        SQL => 'SELECT abc, def FROM some_table WHERE ghi = ?',
        STATEMENT => 'another associated name'
    ));

    if(1 == $OBJECT->prepare(undef,
        INPUT => [
            {
                NAME => 'abc'
            }, {
                NAME => 'def'
            }, {
                NAME => 'ghi'
            }
        ],
        SQL => 'INSERT INTO some_table (abc, def, ghi) VALUES (?, ?, ?);',
        STATEMENT => 'yet another name'
    ));

    if(1 == $OBJECT->channel('PREPARE',
        INPUT => [
            {
                NAME => ''
            }
        ],
        SQL => '',
        STATEMENT => 'and another',
    ));

=over 4

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

=item channel I<(String, Required)>

The abstract identifier of a subroutine.

=item parameters I<(Hash, Required)>

Named parameters.

=over 4

=item INPUT I<Array, Optional>

An array of hashes.  Each hash should contain a I<NAME> key with a string value
that represents the name of a parameter to associate with the corresponding B<?>
I<(Question mark)>.  See the I<bind> method for details.

=item SQL I<(String, Optional)>

The SQL statement to prepare.

=item STATEMENT I<(String, Required)>

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

    $self->{STATEMENTS} = {} if(!defined($self->{STATEMENTS}));
    return 0 if(!defined($parameters{STATEMENT}));
    return 0 if(ref($parameters{STATEMENT}) !~ /^$/);
    if(!defined(${$self->{STATEMENTS}}{$parameters{STATEMENT}})) {
        return 0 if(!defined($parameters{SQL}));
        return 0 if(ref($parameters{SQL}) !~ /^$/);
        $parameters{SQL} =~ s/^\s*(.*)|(.*)\s*$/$1/g;
        my $questionMarks = $parameters{SQL};
        my $questionMarks = $questionMarks =~ s/\?/$1/sg;
        if(0 == $questionMarks) {
            return 0 if(defined($parameters{INPUT}));
        } elsif(!defined($parameters{INPUT})) {
            return 0;
        } elsif(ref($parameters{INPUT}) !~ /^ARRAY$/i) {
            return 0;
        } elsif(scalar(@{$parameters{INPUT}}) != $questionMarks) {
            return 0;
        } else {
            return 0 if(!$self->binding((@{$parameters{INPUT}})));
        }
        my $handle;
        eval {
            $handle = $self->{HANDLE}->prepare($parameters{SQL});
            1;
        } or do {
            $self->rollback();
            return 0;
        };
        my %statement = (
            HANDLE => $handle,
            SQL => $parameters{SQL},
        );
        $statement{INPUT} = $parameters{INPUT} if(defined($parameters{INPUT}));
        ${$self->{STATEMENTS}}{$parameters{STATEMENT}} = \%statement;
    }
    return 1 if(defined($channel));
    return ${$self->{STATEMENTS}}{$parameters{STATEMENT}};
}

Anansi::DatabaseComponent::addChannel('Anansi::DatabaseComponent', 'PREPARE' => 'prepare');


=head2 removeChannel

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

=item B<"SQL">

Describes a specific SQL statement and it's interaction requirements.

=item B<"TEST">

Describes whether to perform the subsequent sequence statement.

=back

=item INPUT I<(Array B<or> String, Optional)>

An array of strings or a string containing the variables or variable data.  Only
used when I<COMMAND> contains B<"LOOP">, B<"RUN"> or B<"SQL">.

=item NAME I<(String, Optional)

A sequence name associated with the SQL statement, control structure or external
process trigger.  When I<COMMAND> is B<"LOOP">, represents the name of a script
variable hash that will contain the current I<ITERATION> of the loop starting at
B<0> I<(zero)>, how many I<ITERATIONS> the loop is expected to perform a B<1>
I<(one)> or greater or a B<0> I<(zero)> for indeterminate and the I<INPUT> value
to use in the iteration or B<undef> if indeterminate.  When I<COMMAND> is B<"RUN">
, represents the name of a script variable that will contain the results of the
trigger.  When I<COMMAND> is B<"TEST">, represents the name of a script variable
that will either contain the result of the current test or the name of a script
variable that should be used to determine whether the sequence statement should
be performed.

=item SQL I<(String, Optional)>

A SQL statement like that described in the I<statement> method's I<SQL>

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

Anansi::DatabaseComponent::addChannel('Anansi::DatabaseComponent', 'SCRIPT' => 'script');

################################################################################

=end comment


=head2 statement

    my $result = Anansi::DatabaseComponent::statement($OBJECT, undef,
        INPUT => [
            'hij' => 'someParameter',
            'klm' => 'anotherParameter'
        ],
        SQL => 'SELECT abc, def FROM some_table WHERE hij = ? AND klm = ?;',
        STATEMENT => 'someStatement',
        someParameter => 123,
        anotherParameter => 456
    );

    my $result = Anansi::DatabaseComponent::channel($OBJECT, 'STATEMENT',

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

=item channel I<(String, Required)>

The abstract identifier of a subroutine.

=item parameters I<(Hash, Optional)>

Named parameters.

=over 4

=item INPUT I<(Array, Optional)>

An array of hashes with each element corresponding to an equivalent B<?>
I<(Question mark)> found within the supplied I<SQL>.  If the number of elements
is not the same as the number of B<?> I<(Question mark)>s found in the statement
then the statement is invalid.  See the I<bind> method for details.

=item SQL I<(String, Optional)>

The SQL statement to execute.

lib/Anansi/DatabaseComponent.pm  view on Meta::CPAN

=cut


sub statement {
    my ($self, $channel, %parameters) = @_;
    return 0 if(ref($self) =~ /^(|ARRAY|CODE|FORMAT|GLOB|HASH|IO|LVALUE|REF|Regexp|SCALAR|VSTRING)$/i);
    my $prepared = $self->prepare(undef, (%parameters));
    my $handle;
    if($prepared) {
        $handle = ${$prepared}{HANDLE};
        if(defined(${$prepared}{INPUT})) {
            my $bound = $self->bind(
                HANDLE => $handle,
                INPUT => ${$prepared}{INPUT},
                VALUE => \%parameters,
            );
            return 0 if(!$bound);
        }
    } else {
        eval {
            $handle = $self->{HANDLE}->prepare($parameters{SQL});
            1;
        } or do {
            $self->rollback();
            return 0;
        };
        my $questionMarks = $parameters{SQL};
        my $questionMarks = $questionMarks =~ s/\?/$1/sg;
        if(0 == $questionMarks) {
            if(defined($parameters{INPUT})) {
                $self->rollback();
                return 0;
            }
        } elsif(!defined($parameters{INPUT})) {
            $self->rollback();
            return 0;
        } elsif(ref($parameters{INPUT}) !~ /^ARRAY$/i) {
            $self->rollback();
            return 0;
        } elsif(scalar(@{$parameters{INPUT}}) != $questionMarks) {
            $self->rollback();
            return 0;
        } else {
            if(!$self->bind(
                HANDLE => $handle,
                INPUT => $parameters{INPUT},
                VALUE => \%parameters,
            )) {
                $self->rollback();
                return 0;
            }
        }
    }
    eval {
        $handle->execute();
        1;



( run in 0.420 second using v1.01-cache-2.11-cpan-4e96b696675 )