File Coverage

blib/lib/Kelp/Response.pm
Criterion Covered Total %
statement 100 106 94.3
branch 28 32 87.5
condition 23 26 88.4
subroutine 26 28 92.8
pod 18 21 85.7
total 195 213 91.5


line stmt bran cond sub pod time code
1             package Kelp::Response;
2              
3 22     22   640 use Kelp::Base 'Plack::Response';
  22         54  
  22         205  
4              
5 22     22   1615 use Encode;
  22         74  
  22         1489  
6 22     22   184 use Carp;
  22         58  
  22         1048  
7 22     22   139 use Try::Tiny;
  22         45  
  22         1047  
8 22     22   149 use Scalar::Util;
  22         74  
  22         35445  
9              
10             attr -app => sub { croak "app is required" };
11             attr rendered => 0;
12             attr partial => 0;
13              
14             sub new {
15 203     203 1 741 my ( $class, %args ) = @_;
16 203         717 my $self = $class->SUPER::new();
17 203         2880 $self->{$_} = $args{$_} for keys %args;
18 203         864 return $self;
19             }
20              
21             sub set_content_type {
22 193     193 1 561 $_[0]->content_type( $_[1] );
23 193         3094 return $_[0];
24             }
25              
26             sub text {
27 5     5 1 25 $_[0]->set_content_type( 'text/plain; charset=' . $_[0]->app->charset );
28             }
29              
30             sub html {
31 156     156 1 438 $_[0]->set_content_type( 'text/html; charset=' . $_[0]->app->charset );
32             }
33              
34             sub json {
35 29     29 1 73 $_[0]->set_content_type('application/json');
36             }
37              
38             sub xml {
39 1     1 1 3 $_[0]->set_content_type('application/xml');
40             }
41              
42             sub finalize {
43 200     200 1 370 my $self = shift;
44 200         615 my $arr = $self->SUPER::finalize(@_);
45 200 100       19463 pop @$arr if $self->partial;
46 200         1709 return $arr;
47             }
48              
49             sub set_header {
50 1     1 1 3 my $self = shift;
51 1         9 $self->SUPER::header(@_);
52 1         73 return $self;
53             }
54              
55             sub no_cache {
56 0     0 1 0 my $self = shift;
57 0         0 $self->set_header( 'Cache-Control' => 'no-cache, no-store, must-revalidate' );
58 0         0 $self->set_header( 'Pragma' => 'no-cache' );
59 0         0 $self->set_header( 'Expires' => '0' );
60 0         0 return $self;
61             }
62              
63             sub set_code {
64 203     203 1 1216 my $self = shift;
65 203         644 $self->SUPER::code(@_);
66 203         1479 return $self;
67             }
68              
69             sub render {
70 192     192 1 318 my $self = shift;
71 192   100     466 my $body = shift // '';
72              
73             # Set code 200 if the code has not been set
74 192 100       578 $self->set_code(200) unless $self->code;
75              
76 192         935 my $is_json = $self->content_type eq 'application/json';
77 192   100     5304 my $guess_json = !$self->content_type && ref($body);
78 192   100     3291 my $guess_html = !$self->content_type && !ref($body);
79              
80             # If the content has been determined as JSON, then encode it
81 192 100 100     3538 if ( $is_json || $guess_json ) {
82 31 50       99 die "No JSON decoder" unless $self->app->can('json');
83 31 100       141 die "Data must be a reference" unless ref($body);
84 27         70 my $json = $self->app->json;
85 27         247 $body = $json->encode($body);
86 24 50       114 $body = encode($self->app->charset, $body) unless $json->get_utf8;
87 24 100       97 $self->json if $guess_json;
88 24         70 $self->body( $body );
89             } else {
90 161 100       539 $self->html if $guess_html;
91 161         384 $self->body( encode( $self->app->charset, $body ) );
92             }
93              
94 185         10863 $self->rendered(1);
95 185         679 return $self;
96             }
97              
98             sub render_binary {
99 2     2 1 4 my $self = shift;
100 2   50     7 my $body = shift // '';
101              
102             # Set code 200 if the code has not been set
103 2 50       7 $self->set_code(200) unless $self->code;
104              
105 2 100       6 if ( !$self->content_type ) {
106 1         47 die "Content-type must be explicitly set for binaries";
107             }
108              
109 1         29 $self->body($body);
110 1         9 $self->rendered(1);
111 1         3 return $self;
112             }
113              
114             sub render_error {
115 32     32 1 76 my ( $self, $code, $error ) = @_;
116              
117 32   100     112 $code //= 500;
118 32   100     101 $error //= "Internal Server Error";
119              
120 32         104 $self->set_code($code);
121              
122             # Look for a template and if not found, then show a generic text
123             try {
124 32     32   1444 local $SIG{__DIE__}; # Silence StackTrace
125 32         98 my $filename = "error/$code";
126 32         119 $self->template(
127             $filename, {
128             app => $self->app,
129             error => $error
130             }
131             );
132             }
133             catch {
134 14     14   7444 $self->render("$code - $error");
135 32         257 };
136              
137 32         488 return $self;
138             }
139              
140             sub render_exception {
141 12     12 0 27 my ( $self, $exception ) = @_;
142              
143 12         35 my $code = $exception->code;
144 12         32 my $body = $exception->body;
145              
146 12         33 $self->set_code($code);
147 12 100       34 if ( defined $body ) {
    100          
148 8         23 my $is_html = $self->content_type =~ m{^text/html};
149 8   66     266 my $guess_html = !$self->content_type && !ref($body);
150              
151 8 100 66     184 if ( $is_html || $guess_html ) {
152 4         16 $self->render_error($code, $body);
153             }
154             else {
155 4         11 $self->render($body);
156             }
157              
158             }
159             elsif ( $self->content_type ) {
160 2         64 $self->content_type('');
161             }
162             }
163              
164             sub render_404 {
165 10     10 1 34 $_[0]->render_error( 404, "File Not Found" );
166             }
167              
168             sub render_500 {
169 37     37 1 3214 my ( $self, $error ) = @_;
170              
171 37 100 100     163 if ( !defined $error || $self->app->mode eq 'deployment' ) {
172 12         42 return $self->render_error;
173             }
174              
175             # if render_500 gets blessed object as error stringify it
176 25 100       80 $error = ref $error if Scalar::Util::blessed $error;
177              
178 25         107 return $self->set_code(500)->render($error);
179             }
180              
181             sub render_401 {
182 0     0 0 0 $_[0]->render_error( 401, "Unauthorized" );
183             }
184              
185             sub render_403 {
186 2     2 0 5 $_[0]->render_error( 403, "Forbidden" );
187             }
188              
189             sub redirect {
190 7     7 1 14 my $self = shift;
191 7         21 $self->rendered(1);
192 7         40 $self->SUPER::redirect(@_);
193             }
194              
195             sub redirect_to {
196 7     7 1 19 my ( $self, $where, $args, $code ) = @_;
197 7         19 my $url = $self->app->url_for($where, %$args);
198 7         31 $self->redirect( $url, $code );
199             }
200              
201             sub template {
202 36     36 1 137 my ( $self, $template, $vars, @rest ) = @_;
203              
204             # Add the app object for convenience
205 36         87 $vars->{app} = $self->app;
206              
207             # Do we have a template module loaded?
208 36 50       87 die "No template module loaded"
209             unless $self->app->can('template');
210              
211 36         91 my $output = $self->app->template( $template, $vars, @rest );
212 22         80 $self->render($output);
213             }
214              
215             1;
216              
217             __END__
218              
219             =pod
220              
221             =head1 NAME
222              
223             Kelp::Response - Format an HTTP response
224              
225             =head1 SYNOPSIS
226              
227             Examples of how to use this module make a lot more sense when shown inside
228             route definitions. Note that in the below examples C<$self-E<gt>res>
229             is an instance of C<Kelp::Response>:
230              
231             # Render simple text
232             sub text {
233             my $self = shift;
234             $self->res->text->render("It works!");
235             }
236              
237             # Render advanced HTML
238             sub html {
239             my $self = shift;
240             $self->res->html->render("<h1>It works!</h1>");
241             }
242              
243             # Render a mysterious JSON structure
244             sub json {
245             my $self = shift;
246             $self->res->json->render({ why => 'no' });
247             }
248              
249             # Render the stock 404
250             sub missing {
251             my $self = shift;
252             $self->res->render_404;
253             }
254              
255             # Render a template
256             sub view {
257             my $self = shift;
258             $self->res->template('view.tt', { name => 'Rick James' } );
259             }
260              
261             =head1 DESCRIPTION
262              
263             The L<PSGI> specification requires that each route returns an array with status
264             code, headers and body. C<Plack::Response> already provides many useful methods
265             that deal with that. This module extends C<Plack::Response> to add the tools we
266             need to write graceful PSGI compliant responses. Some methods return C<$self>,
267             which makes them easy to chain.
268              
269             =head1 ATTRIBUTES
270              
271             =head2 rendered
272              
273             Tells if the response has been rendered. This attribute is used internally and
274             unless you know what you're doing, we recommend that you do not use it.
275              
276             =head2 partial
277              
278             Sets partial response. If this attribute is set to a true value, it will cause
279             C<finalize> to return the HTTP status code and headers, but not the body. This is
280             convenient if you intend to stream your content. In the following example, we
281             set C<partial> to 1 and use C<finalize> to get a C<writer> object for streaming.
282              
283             sub stream {
284             my $self = shift;
285             return sub {
286             my $responder = shift;
287              
288             # Stream JSON
289             $self->res->set_code(200)->json->partial(1);
290              
291             # finalize will now return only the status code and headers
292             my $writer = $responder->( $self->res->finalize );
293              
294             # Stream JSON body using the writer object
295             for ( 1 .. 30 ) {
296             $writer->write(qq|{"id":$_}\n|);
297             sleep 1;
298             }
299              
300             # Close the writer
301             $writer->close;
302             };
303             }
304              
305             For more information on how to stream, see the
306             L<PSGI/Delayed-Response-and-Streaming-Body> docs.
307              
308             =head1 METHODS
309              
310             =head2 render
311              
312             This method tries to act smart, without being a control freak. It will fill out
313             the blanks, unless they were previously filled out by someone else. Here is what
314             is does:
315              
316             =over
317              
318             =item
319              
320             If the response code was not previously set, this method will set it to 200.
321              
322             =item
323              
324             If no content-type is previously set, C<render> will set is based on the type of
325             the data rendered. If it's a reference, then the content-type will be set to
326             C<application/json>, otherwise it will be set to C<text/html>.
327              
328             # Will set the content-type to json
329             $res->render( { numbers => [ 1, 2, 3 ] } );
330              
331             =item
332              
333             Last, the data will be encoded with the charset specified by the app.
334              
335             =back
336              
337             =head2 set_content_type
338              
339             Sets the content type of the response and returns C<$self>.
340              
341             # Inside a route definition
342             $self->res->set_content_type('image/png');
343              
344             =head2 text, html, json, xml
345              
346             These methods are shortcuts for C<set_content_type> with the corresponding type.
347             All of them set the content-type header and return C<$self> so they can be
348             chained.
349              
350             $self->res->text->render("word");
351             $self->res->html->render("<p>word</p>");
352             $self->res->json->render({ word => \1 });
353              
354             =head2 set_header
355              
356             Sets response headers. This is a wrapper around L<Plack::Response/header>, which
357             returns C<$self> to allow for chaining.
358              
359             $self->res->set_header('X-Something' => 'Value')->text->render("Hello");
360              
361             =head2 no_cache
362              
363             A convenience method that sets several response headers instructing most
364             browsers to not cache the response.
365              
366             $self->res->no_cache->json->render({ epoch => time });
367              
368             The above response will contain headers that disable caching.
369              
370             =head2 set_code
371              
372             Set the response code.
373              
374             $self->res->set_code(401)->render("Access denied");
375              
376             =head2 render_binary
377              
378             Render binary files, such as images, etc. You must explicitly set the content_type
379             before that.
380              
381             use Kelp::Less;
382              
383             get '/image/:name' => sub {
384             my $content = Path::Tiny::path("$name.jpg")->slurp_raw;
385             res->set_content_type('image/jpeg')->render_binary( $content );
386              
387             # the same, but probably more effective way (PSGI-server dependent)
388             open( my $handle, "<:raw", "$name.png" )
389             or die("cannot open $name: $!");
390             res->set_content_type('image/png')->render_binary( $handle );
391             };
392              
393             =head2 render_error
394              
395             C<render_error( $code, $error )>
396              
397             Renders the specified return code and an error message. This sub will first look
398             for this error template C<error/$code>, before displaying a plain page with the
399             error text.
400              
401             $self->res->render_error(510, "Not Extended");
402              
403             The above code will look for a template named C<views/errors/510.tt>, and if not
404             found it will render this message:
405              
406             510 - Not Extended
407              
408             A return code of 510 will also be set.
409              
410             =head2 render_404
411              
412             A convenience method that sets code 404 and returns "File Not Found".
413              
414             sub some_route {
415             if ( not $self->req->param('ok') ) {
416             return $self->res->render_404;
417             }
418             }
419              
420             If your application's tone is overly friendly or humorous, you will want to create a
421             custom 404 page. The best way to do this is to design your own 404.tt template and
422             put it in the views/error folder.
423              
424             =head2 render_500
425              
426             C<render_500($optional_error)>
427              
428             Renders the stock "500 - Server Error" message.
429             Designing your own 500 page is also possible. All you need to do is add file 500.tt in
430             views/error. Keep in mind that it will only show in C<deployment>. In any other mode,
431             this method will display the optional error, or the stock error message.
432              
433             =head2 redirect_to
434              
435             Redirects the client to a named route or to a given url. In case the route is passed by
436             name, a hash reference with the needed arguments can be passed after the route's name.
437             As a third optional argument, you can enter the desired response code:
438              
439             $self->redirect_to( '/example' );
440             $self->redirect_to( 'catalogue' );
441             $self->redirect_to( 'catalogue', { id => 243 });
442             $self->redirect_to( 'other', {}, 303 );
443              
444             =head2 template
445              
446             This method renders a template. The template should be previously configured by
447             you and included via a module. See L<Kelp::Module::Template> for a template
448             module.
449              
450             sub some_route {
451             my $self = shift;
452             $self->res->template('home.tt', { login => 'user' });
453             }
454              
455             =cut