# ABSTRACT: Exceptions for PONAPI::Server
package PONAPI::Exception;
use Moose;
use Moose::Util qw/find_meta/;
use JSON::MaybeXS;
sub throw {
my $class_or_obj = shift;
die ( blessed $class_or_obj ? $class_or_obj : $class_or_obj->new(@_) );
}
use overload
q{""} => 'as_string',
fallback => 1;
has message => (
is => 'ro',
isa => 'Str',
required => 1,
);
has status => (
is => 'ro',
isa => 'Int',
default => sub { 400 },
);
has bad_request_data => (
is => 'ro',
isa => 'Bool',
);
has sql_error => (
is => 'ro',
isa => 'Bool',
);
has internal => (
is => 'ro',
isa => 'Bool',
);
has json_api_version => (
is => 'ro',
isa => 'Str',
default => sub { '1.0' },
writer => '_set_json_api_version'
);
# Picked from Throwable::Error
sub as_string {
my $self = shift;
return $self->message;
}
sub as_response {
my $self = shift;
my $status = $self->status;
my $detail = $self->message;
if ( $self->sql_error ) {
$detail = "SQL error: $detail";
}
elsif ( $self->bad_request_data ) {
$detail = "Bad request data: $detail";
}
else {
$status = 500;
warn $detail if $detail;
$detail = "A fatal error has occured, please check server logs";
}
return $status, [], +{
jsonapi => { version => $self->json_api_version },
errors => [ { detail => $detail, status => $status } ],
};
}
sub new_from_exception {
my ( $class, $e ) = @_;
return $e if blessed($e) && $e->isa($class);
my %args_for_new = $class->_handle_exception_obj($e);
unless ( $args_for_new{status} and $args_for_new{message} ) {
%args_for_new = (
status => 500,
message => '',
);
warn "$e";
}
return $class->new(%args_for_new);
}
sub _handle_exception_obj {
my ( $self, $e ) = @_;
return unless blessed($e) or $e->isa('Moose::Exception');
if ( $e->isa('Moose::Exception::AttributeIsRequired') ) {
my $attribute = $e->attribute_name;
return _bad_req( "Parameter `$attribute` is required" );
}
elsif (
$e->isa('Moose::Exception::ValidationFailedForTypeConstraint') or
$e->isa('Moose::Exception::ValidationFailedForInlineTypeConstraint')
) {
my $class = find_meta( $e->class_name );
my $attribute = $class->get_attribute( $e->attribute_name );
my $value_nice = JSON::MaybeXS->new->allow_nonref->utf8->canonical->encode( $e->value );
if ( !$attribute ) {
my $attr = $e->attribute_name;
return _bad_req( "Parameter `$attr` got an expected data type: $value_nice" );
}
my $attribute_name = $attribute->name;
my $type_name = _moose_type_to_nice_description( $attribute->{isa} );
return _bad_req( "Parameter `$attribute_name` expected $type_name, but got a $value_nice" );
}
return;
}
sub _bad_req {
return (
message => shift,
status => 400,
bad_request_data => 1,
);
}
# THIS IS NOT COMPLETE, NOR IS IT MEANT TO BE
sub _moose_type_to_nice_description {
my ($type_name) = @_;
$type_name =~ s/ArrayRef/Collection/g;
$type_name =~ s/HashRef/Resource/g;
$type_name =~ s/Maybe\[(.+)]/null or $1/g;
$type_name =~ s/\|/ or /g;
return $type_name;
}
__PACKAGE__->meta->make_immutable;
no Moose; 1;
__END__
=pod
=encoding UTF-8
=head1 NAME
PONAPI::Exception - Exceptions for PONAPI::Server
=head1 VERSION
version 0.003003
=head1 SYNOPSIS
use PONAPI::Exception;
PONAPI::Exception->throw( message => "Generic exception" );
PONAPI::Exception->throw(
message => "Explanation for the sql error, maybe $DBI::errstr",
sql => 1,
);
PONAPI::Exception->throw(
message => "Data had type `foo` but we wanted `bar`",
bad_request_data => 1,
);
=head1 DESCRIPTION
I<PONAPI::Exception> can be used by repositories to signal errors;
exceptions thrown this way will be caught by L<the DAO|PONAPI::DAO> and
handled gracefully.
Different kinds of exceptions can be thrown by changing the arguments
to C<throw>; C<sql =E<gt> 1> will throw a SQL exception,
C<bad_request_data =E<gt> 1> will throw an exception due to the
input data being wrong, and not passing any of those will
throw a generic exception.
The human-readable C<message> for all of those will end up in the
error response returned to the user.
=head1 METHODS
=head2 message
This attribute contains the exception message.
=head2 as_string
Returns a stringified form of the exception. The object is overloaded
to return this if used in string context.
=head2 as_response
Returns the exception as a 3-element list that may be fed directly
to plack as a {json:api} response.
$e->as_response; # ( $status, [], { errors => [ { detail => $message } ] } )
=head2 json_api_version
Defaults to 1.0; only used in C<as_response>.
=head2 status
HTTP Status code for the exception; in most cases you don't need to
set this manually.
=head1 AUTHORS
=over 4
=item *
Mickey Nasriachi <mickey@cpan.org>
=item *
Stevan Little <stevan@cpan.org>
=item *
Brian Fraser <hugmeir@cpan.org>
=back
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2019 by Mickey Nasriachi, Stevan Little, Brian Fraser.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut