# Dist/Zilla/Role/ErrorLogger.pm #

#pod =encoding UTF-8
#pod
#pod =head1 COPYRIGHT AND LICENSE
#pod
#pod Copyright © 2015 Van de Bugger
#pod
#pod This file is part of perl-Dist-Zilla-Role-ErrorLogger.
#pod
#pod perl-Dist-Zilla-Role-ErrorLogger is free software: you can redistribute it and/or modify it
#pod under the terms of the GNU General Public License as published by the Free Software Foundation,
#pod either version 3 of the License, or (at your option) any later version.
#pod
#pod perl-Dist-Zilla-Role-ErrorLogger is distributed in the hope that it will be useful, but WITHOUT
#pod ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
#pod PURPOSE. See the GNU General Public License for more details.
#pod
#pod You should have received a copy of the GNU General Public License along with
#pod perl-Dist-Zilla-Role-ErrorLogger. If not, see <http://www.gnu.org/licenses/>.
#pod
#pod =cut

# --------------------------------------------------------------------------------------------------

#pod =head1 SYNOPSIS
#pod
#pod     package Dist::Zilla::Plugin::YourPlugin;
#pod     use Moose;
#pod     use namespace::autoclean;
#pod     with 'Dist::Zilla::Role::Plugin';
#pod     with 'Dist::Zilla::Role::ErrorLogger';
#pod
#pod     sub method {
#pod         my $self = shift( @_ );
#pod         …
#pod         if ( … ) { $self->log_error( 'error message' ); };
#pod         assertion_condition or $self->log_error( 'another error message' );
#pod         …
#pod         foreach … {
#pod             …
#pod             check or $self->log_error( 'error message' ) and next;
#pod             …
#pod         };
#pod         …
#pod         $self->abort_if_error();
#pod     };
#pod
#pod     __PACKAGE__->meta->make_immutable;
#pod     1;
#pod
#pod =cut

# --------------------------------------------------------------------------------------------------

#pod =head1 DESCRIPTION
#pod
#pod C<Dist::Zilla> limits logging capabilities with 3 logging levels available in plugings through
#pod C<log_debug>, C<log>, and C<log_fatal> methods. Debug level messages are turned off by default, the
#pod first fatal message terminates C<Dist::Zilla>. This is simple, but sometimes you may want to report
#pod all the errors, instead of stopping at the first found one. In such a case C<log_fatal> cannot be
#pod used, obviously. There are few alternatives:
#pod
#pod Collect error messages in an array, then report all the errors with single C<log_fatal> call:
#pod
#pod     my @errors;
#pod     …
#pod     push( @errors, … );
#pod     …
#pod     if ( @errors ) {
#pod         $self->log_fatal( join( "\n", @errors ) );
#pod     };
#pod
#pod This works, but current implementation of C<log_fatal> has a disadvantage: it prints the message
#pod twice, so output will looks ugly. (See L<message handling in log_fatal is
#pod suboptimal|https://github.com/rjbs/Dist-Zilla/issues/397>.)
#pod
#pod Another approach is reporting each error immediately with C<log>, counting number of reported
#pod errors, and calling C<log_fatal> once at the end:
#pod
#pod     my $error_count = 0;
#pod     …
#pod     $self->log( 'error' );
#pod     ++ $error_count;
#pod     …
#pod     if ( $error_count ) {
#pod         $self->log_fatal( 'Aborting...' );
#pod     };
#pod
#pod This works, but incrementing the counter after each C<log> call is boring and error-prone.
#pod C<Dist-Zilla-Role-ErrorLogger> role automates it, making plugin code shorter and more readable:
#pod
#pod     with 'Dist::Zilla::Role::ErrorLogger';
#pod     …
#pod     $self->log_error( 'error' );
#pod     …
#pod     $self->abort_if_error();
#pod
#pod =cut

# --------------------------------------------------------------------------------------------------

package Dist::Zilla::Role::ErrorLogger;

use feature qw{ state };

# ABSTRACT: Provide error logging capabilities for Dist::Zilla plugins

BEGIN {
    our $VERSION = '0.002'; # VERSION
};

use Moose::Role;
use namespace::autoclean;

requires qw{ log log_fatal };

# --------------------------------------------------------------------------------------------------

#pod =attr error_count
#pod
#pod Number of logged errors (i. e. number of C<log_error> calls). Read-only.
#pod
#pod =cut

has error_count => (
    is          => 'ro',
    isa         => 'Int',
    default     => 0,
    init_arg    => undef,
);

# --------------------------------------------------------------------------------------------------

#pod =method log_error
#pod
#pod This method calls C<log> method, passing all the arguments, and increments value of C<error_count>
#pod attribute. The method returns true value, so can be used in following constructs:
#pod
#pod     while ( … ) {
#pod         …
#pod         assert_condition or $self->log_error( 'message' ) and next;
#pod         …
#pod     };
#pod
#pod Messages are reported at C<error> level, if level is not explicitly specified. (It seems level is
#pod not used by C<Dist::Zilla> now.) Custom arguments are respected, too:
#pod
#pod     $self->log_error( { prefix => 'subsystem: ', level => 'info' }, $message );
#pod
#pod (See L<Log::Dispatchouli/log> for more details. Err… Unfortunately, there are very little details
#pod there.)
#pod
#pod =cut

sub log_error {
    my $self = shift( @_ );
    #   If the first argument is a hashref, it is treated as extra arguments to logging, not as
    #   message, see <https://metacpan.org/pod/Log::Dispatchouli#log>. Let us add `level` argument.
    #   However, we should not change user-supplied hash and arguments.
    state $level = {
        level => 'error',
    };
    my $arg;
    if ( ref( $_[ 0 ] ) eq 'HASH' ) {
        #   User provided hashref, remove it from `@_`.
        $arg = shift( @_ );
        #   Do not override user-supplied `level` argument, if any.
        if ( not exists( $arg->{ level } ) ) {
            #   User did not provide `level` argument, so we need to add it.
            #   Note: new hash created intentionally, to keep user-supplied hash intact.
            $arg = { %$level, %$arg };
        };
    } else {
        #   User did not provide hashref, just use ours.
        $arg = $level;
    };
    $self->log( $arg, @_ );
    ++ $self->{ error_count };
    return $self->{ error_count };
};

# --------------------------------------------------------------------------------------------------

#pod =method abort_if_error
#pod
#pod If there was any errors (i. e. C<error_count> is greater than zero), the method aborts execution by
#pod calling C<log_fatal> with all the arguments. If the method is called with no arguments, it pass
#pod to C<logh_fatal> string "Aborting...".
#pod
#pod =cut

sub abort_if_error {
    my $self = shift( @_ );
    if ( $self->error_count ) {
        $self->log_fatal( @_ ? @_ : 'Aborting...' );
            #   `@_ or 'Aborting...'` looks shorter, but it evaluates `@_` in scalar context.
    };
};

1;

# end of file #

__END__

=pod

=encoding UTF-8

=head1 NAME

Dist::Zilla::Role::ErrorLogger - Provide error logging capabilities for Dist::Zilla plugins

=head1 VERSION

Version 0.002.

=head1 SYNOPSIS

    package Dist::Zilla::Plugin::YourPlugin;
    use Moose;
    use namespace::autoclean;
    with 'Dist::Zilla::Role::Plugin';
    with 'Dist::Zilla::Role::ErrorLogger';

    sub method {
        my $self = shift( @_ );
        …
        if ( … ) { $self->log_error( 'error message' ); };
        assertion_condition or $self->log_error( 'another error message' );
        …
        foreach … {
            …
            check or $self->log_error( 'error message' ) and next;
            …
        };
        …
        $self->abort_if_error();
    };

    __PACKAGE__->meta->make_immutable;
    1;

=head1 DESCRIPTION

C<Dist::Zilla> limits logging capabilities with 3 logging levels available in plugings through
C<log_debug>, C<log>, and C<log_fatal> methods. Debug level messages are turned off by default, the
first fatal message terminates C<Dist::Zilla>. This is simple, but sometimes you may want to report
all the errors, instead of stopping at the first found one. In such a case C<log_fatal> cannot be
used, obviously. There are few alternatives:

Collect error messages in an array, then report all the errors with single C<log_fatal> call:

    my @errors;
    …
    push( @errors, … );
    …
    if ( @errors ) {
        $self->log_fatal( join( "\n", @errors ) );
    };

This works, but current implementation of C<log_fatal> has a disadvantage: it prints the message
twice, so output will looks ugly. (See L<message handling in log_fatal is
suboptimal|https://github.com/rjbs/Dist-Zilla/issues/397>.)

Another approach is reporting each error immediately with C<log>, counting number of reported
errors, and calling C<log_fatal> once at the end:

    my $error_count = 0;
    …
    $self->log( 'error' );
    ++ $error_count;
    …
    if ( $error_count ) {
        $self->log_fatal( 'Aborting...' );
    };

This works, but incrementing the counter after each C<log> call is boring and error-prone.
C<Dist-Zilla-Role-ErrorLogger> role automates it, making plugin code shorter and more readable:

    with 'Dist::Zilla::Role::ErrorLogger';
    …
    $self->log_error( 'error' );
    …
    $self->abort_if_error();

=head1 ATTRIBUTES

=head2 error_count

Number of logged errors (i. e. number of C<log_error> calls). Read-only.

=head1 METHODS

=head2 log_error

This method calls C<log> method, passing all the arguments, and increments value of C<error_count>
attribute. The method returns true value, so can be used in following constructs:

    while ( … ) {
        …
        assert_condition or $self->log_error( 'message' ) and next;
        …
    };

Messages are reported at C<error> level, if level is not explicitly specified. (It seems level is
not used by C<Dist::Zilla> now.) Custom arguments are respected, too:

    $self->log_error( { prefix => 'subsystem: ', level => 'info' }, $message );

(See L<Log::Dispatchouli/log> for more details. Err… Unfortunately, there are very little details
there.)

=head2 abort_if_error

If there was any errors (i. e. C<error_count> is greater than zero), the method aborts execution by
calling C<log_fatal> with all the arguments. If the method is called with no arguments, it pass
to C<logh_fatal> string "Aborting...".

=head1 AUTHOR

Van de Bugger <van.de.bugger@gmail.com>

=head1 COPYRIGHT AND LICENSE

Copyright © 2015 Van de Bugger

This file is part of perl-Dist-Zilla-Role-ErrorLogger.

perl-Dist-Zilla-Role-ErrorLogger is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.

perl-Dist-Zilla-Role-ErrorLogger is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with
perl-Dist-Zilla-Role-ErrorLogger. If not, see <http://www.gnu.org/licenses/>.

=cut
