File Coverage

blib/lib/Catalyst/ResponseHelpers.pm
Criterion Covered Total %
statement 35 95 36.8
branch 0 20 0.0
condition 0 33 0.0
subroutine 12 25 48.0
pod 13 13 100.0
total 60 186 32.2


line stmt bran cond sub pod time code
1 1     1   16688 use 5.010;
  1         3  
2 1     1   3 use strict;
  1         1  
  1         19  
3 1     1   3 use warnings;
  1         2  
  1         21  
4 1     1   636 use utf8;
  1         10  
  1         4  
5             package Catalyst::ResponseHelpers;
6 1     1   530 use parent qw< Exporter::Tiny >;
  1         263  
  1         4  
7 1     1   3858 use HTTP::Status qw< :constants :is status_message >;
  1         3508  
  1         458  
8 1     1   854 use Path::Tiny;
  1         12070  
  1         49  
9 1     1   385 use Safe::Isa qw< $_isa >;
  1         373  
  1         82  
10 1     1   459 use Encode qw< encode_utf8 >;
  1         6675  
  1         51  
11 1     1   377 use IO::String;
  1         2583  
  1         23  
12 1     1   464 use URI;
  1         2932  
  1         22  
13 1     1   355 use URI::QueryParam;
  1         457  
  1         700  
14              
15             our $VERSION = '1.01';
16              
17             =encoding utf-8
18              
19             =head1 NAME
20              
21             Catalyst::ResponseHelpers - Concise response constructors for Catalyst controllers
22              
23             =head1 SYNOPSIS
24              
25             use Catalyst::ResponseHelpers qw< :helpers :status >;
26              
27             sub show_user : Chained('/') PathPart('user') Args(1) {
28             my ($c, $id) = @_;
29             my $user = load_user($id)
30             or return NotFound($c, "The user id <$id> couldn't be found.");
31             ...
32             }
33              
34             =head1 DESCRIPTION
35              
36             Various helper functions for setting up the current L<Catalyst::Response>
37             object. All response helpers call C<Catalyst/detach> to stop request
38             processing. For clarity in your controller actions, it is nevertheless
39             recommended that you call these helpers as values to L<return()|perlfunc/return>.
40              
41             =head1 EXPORTS
42              
43             By default, only the helper methods documented below are exported. You may
44             explicitly request them using the C<:helpers> tag.
45              
46             You may also request C<:status>, which re-exports the C<:constants> from
47             L<HTTP::Status> into your package. This is useful for custom status codes.
48              
49             =cut
50              
51             our %EXPORT_TAGS = (
52             status => $HTTP::Status::EXPORT_TAGS{constants},
53             helpers => [qw[
54             Ok
55             Forbidden
56             NotFound
57             ClientError
58             ServerError
59             TextPlain
60             AsJSON
61             FromFile
62             FromCharString
63             FromHandle
64             Redirect
65             RedirectToUrl
66             ReturnWithMsg
67             ]],
68             );
69             our @EXPORT = @{ $EXPORT_TAGS{helpers} };
70             our @EXPORT_OK = map { @$_ } values %EXPORT_TAGS;
71              
72             =head1 FUNCTIONS
73              
74             =head2 ReturnWithMsg($c, $mid)
75              
76             Redirects to the request’s C<return> parameter, or C</> if no such parameter
77             exists or if the given URI appears to be external to the app. The given
78             C<$mid> is set as a query parameter, and should be the result of a
79             C<< $c->set_status_msg >> or C<< $c->set_error_msg >> call. These context
80             methods are normally provided by L<Catalyst::Plugin::StatusMessage>.
81              
82             =head2 Redirect($c, $action_or_action_path, @args?)
83              
84             Passes arguments to L<Catalyst/uri_for_action> and redirects to the returned
85             URL.
86              
87             =head2 RedirectToUrl($c, $url, $status?)
88              
89             Redirects to the given URL, with an optional custom status. Status defaults to
90             302 (HTTP_FOUND).
91              
92             =cut
93              
94             sub ReturnWithMsg {
95 0     0 1   my ($c, $mid) = @_;
96 0           my $base = $c->req->base;
97 0           my $return = URI->new( $c->req->param('return') );
98 0 0 0       $return = $c->uri_for('/') unless $return and $return =~ m{^\Q$base\E}i;
99 0           $return->query_param_append( mid => $mid );
100 0           RedirectToUrl($c, $return);
101             }
102              
103             sub Redirect {
104 0     0 1   my ($c, $action, @rest) = @_;
105 0           RedirectToUrl($c, $c->uri_for_action($action, @rest));
106             }
107              
108             sub RedirectToUrl {
109 0     0 1   my ($c, $url, $status) = @_;
110 0           $c->response->redirect($url, $status);
111 0           $c->detach;
112             }
113              
114             =head2 Ok($c, $status?, $msg?)
115              
116             Sets a body-less 204 No Content response by default, switching to a 200 OK with
117             a body via L</TextPlain> iff a message is provided. Both the status and
118             message may be omitted or provided. If the message is omitted, a body-less
119             response is set.
120              
121             Note that if you're using L<Catalyst::Action::RenderView> and you specify a
122             status other than 204 but don't provide a message (e.g. C<Ok($c, 200)>),
123             RenderView will intercept the response and try to render a template. This
124             probably isn't what you wanted. A workaround is to use the proper status code
125             when sending no content (204) or specify a message (the empty string is OK).
126              
127             =cut
128              
129             sub Ok {
130 0     0 1   my ($c, $status, $msg) = @_;
131 0 0 0       ($status, $msg) = (undef, $status)
132             if @_ == 2 and not is_success($status);
133              
134 0 0         if (defined $msg) {
135 0   0       $status //= HTTP_OK;
136 0           TextPlain($c, $status, $msg);
137             } else {
138 0   0       $status //= HTTP_NO_CONTENT;
139 0           $c->response->status($status);
140 0           $c->response->body(undef);
141 0           $c->detach;
142             }
143             }
144              
145             =head2 Forbidden($c, $msg?)
146              
147             Sets a plain text 403 Forbidden response, with an optional custom message.
148              
149             =head2 NotFound($c, $msg?)
150              
151             Sets a plain text 404 Not Found response, with an optional custom message.
152              
153             =cut
154              
155             sub Forbidden {
156 0     0 1   my ($c, $msg) = @_;
157 0           TextPlain($c, HTTP_FORBIDDEN, $msg);
158             }
159              
160             sub NotFound {
161 0     0 1   my ($c, $msg) = @_;
162 0           TextPlain($c, HTTP_NOT_FOUND, $msg);
163             }
164              
165             =head2 ClientError($c, $status?, $msg?)
166              
167             Sets a plain text 400 Bad Request response by default, with an optional
168             custom message. Both the status and message may be omitted or provided.
169              
170             =head2 ServerError($c, $status?, $msg?)
171              
172             Sets a plain text 500 Internal Server Error response by default, with an
173             optional custom message. Both the status and message may be omitted or
174             provided. The error is logged via L<Catalyst/log>.
175              
176             =cut
177              
178             sub ClientError {
179 0     0 1   my ($c, $status, $msg) = @_;
180 0 0 0       ($status, $msg) = (undef, $status)
181             if @_ == 2 and not is_client_error($status);
182 0   0       TextPlain($c, $status // HTTP_BAD_REQUEST, $msg);
183             }
184              
185             sub ServerError {
186 0     0 1   my ($c, $status, $msg) = @_;
187 0 0 0       ($status, $msg) = (undef, $status)
188             if @_ == 2 and not is_server_error($status);
189 0   0       $status //= HTTP_INTERNAL_SERVER_ERROR;
190 0           $c->log->error("HTTP $status: $msg");
191 0           TextPlain($c, $status, $msg);
192             }
193              
194             =head2 TextPlain($c, $status?, $msg?)
195              
196             Sets a plain text 200 OK response by default, with an optional custom
197             message. Both the status and message may be omitted or provided.
198              
199             =cut
200              
201             sub TextPlain {
202 0     0 1   my ($c, $status, $msg) = @_;
203 0 0 0       ($status, $msg) = (undef, $status)
204             if @_ == 2 and not status_message($status);
205 0   0       $status //= HTTP_OK;
206 0           $c->response->status($status);
207 0           $c->response->content_type("text/plain");
208 0   0       $c->response->body($msg // status_message($status));
209 0           $c->detach;
210             }
211              
212             =head2 AsJSON($c, $status?, $data)
213              
214             Sets a JSON 200 OK response by default, with an optional custom status. Data
215             should be serializable by a view named C<JSON> provided by your application
216             (e.g. via L<Catalyst::View::JSON>).
217              
218             =cut
219              
220             sub AsJSON {
221 0     0 1   my ($c, $status, $data) = @_;
222 0 0         ($status, $data) = (undef, $status)
223             if @_ == 2;
224 0   0       $status //= HTTP_OK;
225 0           $c->response->status($status);
226 0           $c->stash( json => $data );
227 0           $c->view('JSON')->process($c);
228 0           $c->detach;
229             }
230              
231             =head2 FromFile($c, $filename, $mime_type, $headers?)
232              
233             Sets a response from the contents of the filename using the specified MIME
234             type. C<Content-Length> and C<Last-Modified> are set from the file.
235              
236             The C<Content-Disposition> is set to C<attachment> by default, usually forcing
237             a download.
238              
239             An optional arrayref of additional headers may also be provided, which is
240             passed through to L</FromHandle>.
241              
242             =head2 FromCharString($c, $string, $mime_type, $headers?)
243              
244             Sets a response from the contents of a B<character> string using the specified
245             MIME type. The character string will be encoded as UTF-8 bytes.
246              
247             The C<Content-Disposition> is set to C<attachment> by default, usually forcing
248             a download.
249              
250             An optional arrayref of additional headers may also be provided, which is
251             passed through to L</FromHandle>.
252              
253             =head2 FromHandle($c, $handle, $mime_type, $headers?)
254              
255             Sets a response from the contents of the filehandle using the specified MIME
256             type. An optional arrayref of additional headers may also be provided, which
257             is passed to L<the response’s|Catalyst::Response> L<HTTP::Headers> object.
258              
259             The C<Content-Disposition> is set to C<attachment> by default, usually forcing
260             a download.
261              
262             =cut
263              
264             sub FromFile {
265 0     0 1   my ($c, $file) = (shift, shift);
266 0 0         $file = path($file)
267             unless $file->$_isa("Path::Tiny");
268 0           return FromHandle($c, $file->openr_raw, @_);
269             }
270              
271             sub FromCharString {
272 0     0 1   my ($c, $string) = (shift, shift);
273 0           my $handle = IO::String->new( encode_utf8($string) );
274 0           return FromHandle($c, $handle, @_);
275             }
276              
277             sub FromHandle {
278 0     0 1   my ($c, $handle, $mime, $headers) = @_;
279 0           my $h = $c->response->headers;
280              
281 0           $c->response->body( $handle );
282 0           $c->response->header('Content-Disposition' => 'attachment');
283              
284             # Default to UTF-8 for text content unless otherwise specified
285 0           $h->content_type( $mime );
286 0 0 0       $h->content_type( "$mime; charset=utf-8" )
287             if $h->content_is_text and not $h->content_type_charset;
288              
289 0 0         $h->header( @$headers )
290             if $headers;
291 0           $c->detach;
292             }
293              
294             =head1 AUTHOR
295              
296             Thomas Sibley E<lt>trsibley@uw.eduE<gt>
297              
298             =head1 THANKS
299              
300             Inspired in part by seeing John Napiorkowski’s (jnap)
301             L<experimental response helpers in CatalystX::Example::Todo|https://github.com/jjn1056/CatalystX-Example-Todo/blob/master/lib/Catalyst/ResponseHelpers.pm>.
302              
303             =head1 COPYRIGHT
304              
305             Copyright 2015- by the University of Washington
306              
307             =head1 LICENSE
308              
309             This library is free software; you can redistribute it and/or modify
310             it under the same terms as Perl itself.
311              
312             =cut
313              
314             1;