File Coverage

File:lib/Yukki/Error.pm
Coverage:91.3%

linestmtbrancondsubpodtimecode
1package Yukki::Error;
2
3
7
7
55
18
use v5.24;
4
7
7
7
21
7
38
use utf8;
5
7
7
7
328
5186
27
use Moo;
6
7extends qw( HTTP::Throwable::Factory );
8
9
7
7
7
3803
31
145
use Yukki::Web::View;
10
11
7
7
7
30
9
33
use namespace::clean;
12
13
7
2212
use Sub::Exporter -setup => {
14    exports => [ qw< http_throw http_exception > ],
15
7
7
14057
68
};
16
17# ABSTRACT: Yukki's exception class
18
19 - 59
=head1 SYNOPSIS

  Yukki::Error->throw("Something really bad.", { ... });

=head1 DESCRIPTION

If you are familiar with L<HTTP::Throwable::Factory>, this is similar to that (and is based on that).

However, there are two differences. First, the error message is given primacy rather than exception type, so you can just use this to throw an exception:

    use Yukki::Error qw( http_throw );
    http_throw('something went wrong');

Since you almost always want your exception to be an internal server error of some kind, this makes more sense to me than having to write:

    use HTTP::Throwable::Factory qw( http_throw );
    http_throw(InternalServerError => {
        message => 'something went wrong',
    });

To specify the type of exception, us C<status>:

    use Yukki::Error qw( http_throw );
    http_throw('something was not found', {
        status => 'NotFound',
    });

The second difference is that all exceptions thrown by this factory inherit from L<Yukki::Error>, so this works:

    use Scalar::Util qw( blessed );
    use Try::Tiny;
    try { ... }
    catch {
        if (blassed $_ && $_->isa("Yukki::Error") {
            # we now this is an application error from Yukki
        }
    };

This makes it easy to know whether Yukki generated the exception or something else did.

=cut
60
61
2
1
21241
sub base_class { 'Yukki::Error' }
62
2
1
18
sub extra_roles { 'Yukki::Error::Body' }
63
64 - 75
=head1 EXPORTS

=head2 http_exception

  my $error = http_exception('message', {
      status           => 'InternalServerError',
      show_stask_trace => 0,
  });

Creates a new exception object. Calls the constructor for L<Yukki:Error> and applied the L<HTTP::Throwable> status role needed (prior to construction actually).

=cut
76
77sub http_exception {
78
2
1
5
    my ($name, $args) = @_;
79
80
2
2
3
17
    my %args = %{ $args // {} };
81
2
6
    my $status = delete $args{status} // 'InternalServerError';
82
83
2
23
    Yukki::Error->new_exception($status => {
84        %args,
85        message => "$name",
86    });
87}
88
89 - 98
=head2 http_throw

  http_throw('message', {
      status           => 'InternalServerError',
      show_stask_trace => 0,
  });

Constructs the exception (via L</http_exception>) and throws it.

=cut
99
100sub http_throw {
101
2
1
4
    my ($name, $args) = @_;
102
103
2
6
    http_exception($name, $args)->throw;
104}
105
106sub BUILDARGS {
107
2
0
21852
    my ($class, $args) = @_;
108
2
26
    $args;
109}
110
111 - 119
=begin Pod::Coverage

    BUILDARGS
    base_class
    extra_roles

=end Pod::Coverage

=cut
120
121{
122    package Yukki::Error::Body;
123
124
7
7
7
3344
9
25
    use Moo::Role;
125
126 - 132
=head1 METHODS

=head2 body

Renders the HTML body for the error.

=cut
133
134    sub body {
135
1
0
2
        my ($self, $env) = @_;
136
137
1
3
        my $app  = $env->{'yukki.app'};
138
1
7
        my $view = Yukki::Web::View->new(app => $app);
139
1
141
        my $ctx  = Yukki::Web::Context->new(env => $env);
140
141
1
25
        my $template = $view->prepare_template(
142            template   => 'error.html',
143            directives => [
144                '#error-page' => 'error_message',
145            ],
146        );
147
148
1
603
        $ctx->response->page_title($self->reason);
149
150
1
100
        return $view->render_page(
151            template => $template,
152            context  => $ctx,
153            vars     => {
154                'error_message' => $self->message,
155            },
156        );
157    }
158
159 - 163
=head2 body_headers

Setup the HTTP headers.

=cut
164
165    sub body_headers {
166
1
0
30
        my ($self, $body) = @_;
167
168        return [
169
1
4
            'Content-type'   => 'text/html',
170            'Content-length' => length $body,
171        ];
172    }
173
174 - 178
=head2 as_string

Returns the message.

=cut
179
180    sub as_string {
181
7
0
396
        my $self = shift;
182
7
15
        return $self->message;
183    }
184
185    around as_psgi => sub {
186        shift; # original method is ignored
187        my ($self, $env) = @_;
188        my $body    = $self->body( $env );
189        my $headers = $self->build_headers( $body );
190        [ $self->status_code, $headers, [ defined $body ? $body : () ] ];
191    };
192
193}
194
1951;