File Coverage

blib/lib/Web/Response.pm
Criterion Covered Total %
statement 90 90 100.0
branch 33 40 82.5
condition 9 11 81.8
subroutine 19 19 100.0
pod 4 4 100.0
total 155 164 94.5


line stmt bran cond sub pod time code
1             package Web::Response;
2             BEGIN {
3 20     20   185698 $Web::Response::AUTHORITY = 'cpan:DOY';
4             }
5             {
6             $Web::Response::VERSION = '0.11';
7             }
8 20     20   3539 use Moose;
  20         3215084  
  20         152  
9             # ABSTRACT: common response class for web frameworks
10              
11 20     20   139517 use HTTP::Headers ();
  20         34946  
  20         408  
12 20     20   3757 use Plack::Util ();
  20         54867  
  20         335  
13 20     20   2028 use URI::Escape ();
  20         6007  
  20         344  
14              
15 20     20   7098 use Web::Request::Types ();
  20         85  
  20         19676  
16              
17              
18             has status => (
19             is => 'rw',
20             isa => 'Web::Request::Types::HTTPStatus',
21             lazy => 1,
22             default => sub { confess "Status was not supplied" },
23             );
24              
25             has headers => (
26             is => 'rw',
27             isa => 'Web::Request::Types::HTTP::Headers',
28             lazy => 1,
29             coerce => 1,
30             default => sub { HTTP::Headers->new },
31             handles => {
32             header => 'header',
33             content_length => 'content_length',
34             content_type => 'content_type',
35             content_encoding => 'content_encoding',
36             location => [ header => 'Location' ],
37             },
38             );
39              
40             has content => (
41             is => 'rw',
42             isa => 'Web::Request::Types::PSGIBody',
43             lazy => 1,
44             coerce => 1,
45             default => sub { [] },
46             );
47              
48             has streaming_response => (
49             is => 'rw',
50             isa => 'CodeRef',
51             predicate => 'has_streaming_response',
52             );
53              
54             has cookies => (
55             traits => ['Hash'],
56             is => 'rw',
57             isa => 'HashRef[Str|HashRef[Str]]',
58             lazy => 1,
59             default => sub { +{} },
60             handles => {
61             has_cookies => 'count',
62             },
63             );
64              
65             has _encoding_obj => (
66             is => 'rw',
67             isa => 'Object',
68             predicate => 'has_encoding',
69             handles => {
70             encoding => 'name',
71             },
72             );
73              
74             sub BUILDARGS {
75 56     56 1 121 my $class = shift;
76              
77 56 100 100     519 if (@_ == 1 && ref($_[0]) eq 'ARRAY') {
    100 66        
78             return {
79             status => $_[0][0],
80 12         37 (@{ $_[0] } > 1
81             ? (headers => $_[0][1])
82             : ()),
83 12 100       28 (@{ $_[0] } > 2
  12 50       278  
84             ? (content => $_[0][2])
85             : ()),
86             };
87             }
88             elsif (@_ == 1 && ref($_[0]) eq 'CODE') {
89             return {
90 12         254 streaming_response => $_[0],
91             };
92             }
93             else {
94 32         246 return $class->SUPER::BUILDARGS(@_);
95             }
96             }
97              
98             sub redirect {
99 3     3 1 14 my $self = shift;
100 3         7 my ($url, $status) = @_;
101              
102 3   100     72 $self->status($status || 302);
103 3         17 $self->location($url);
104             }
105              
106             sub finalize {
107 51     51 1 371 my $self = shift;
108              
109 51 100       1552 return $self->_finalize_streaming
110             if $self->has_streaming_response;
111              
112             my $res = [
113             $self->status,
114             [
115             map {
116 39         876 my $k = $_;
  15         280  
117             map {
118 15         66 my $v = $_;
  15         788  
119             # replace LWS with a single SP
120 15         45 $v =~ s/\015\012[\040|\011]+/chr(32)/ge;
  2         8  
121             # remove CR and LF since the char is invalid here
122 15         79 $v =~ s/\015|\012//g;
123 15         373 ( $k => $v )
124             } $self->header($k);
125             } $self->headers->header_field_names
126             ],
127             $self->content
128             ];
129              
130 39         169 $self->_finalize_cookies($res);
131              
132 39 100       1088 return $res unless $self->has_encoding;
133              
134             return Plack::Util::response_cb($res, sub {
135             return sub {
136 29         615 my $chunk = shift;
137 29 100       112 return unless defined $chunk;
138 8         28 return $self->_encode($chunk);
139 21     21   419 };
140 21         175 });
141             }
142              
143             sub to_app {
144 1     1 1 5 my $self = shift;
145 1     1   9 return sub { $self->finalize };
  1         15106  
146             }
147              
148             sub _finalize_streaming {
149 12     12   25 my $self = shift;
150              
151 12         260 my $streaming = $self->streaming_response;
152              
153 12 100 66     286 return $streaming
154             unless $self->has_encoding || $self->has_cookies;
155              
156             return Plack::Util::response_cb($streaming, sub {
157 10     10   2374 my $res = shift;
158 10         34 $self->_finalize_cookies($res);
159 10 50       249 return unless $self->has_encoding;
160             return sub {
161 25         835 my $chunk = shift;
162 25 100       63 return unless defined $chunk;
163 15         36 return $self->_encode($chunk);
164 10         40 };
165 10         87 });
166             }
167              
168             sub _encode {
169 23     23   46 my $self = shift;
170 23         56 my ($content) = @_;
171 23 50       592 return $content unless $self->has_encoding;
172 23         468 return $self->_encoding_obj->encode($content);
173             }
174              
175             sub _finalize_cookies {
176 49     49   94 my $self = shift;
177 49         116 my ($res) = @_;
178              
179 49         1190 my $cookies = $self->cookies;
180 49         153 for my $name (keys %$cookies) {
181 23         68 push @{ $res->[1] }, (
182 23         35 'Set-Cookie' => $self->_bake_cookie($name, $cookies->{$name}),
183             );
184             }
185              
186 49         1162 $self->cookies({});
187             }
188              
189             sub _bake_cookie {
190 23     23   38 my $self = shift;
191 23         46 my ($name, $val) = @_;
192              
193 23 50       52 return '' unless defined $val;
194 23 50       54 $val = { value => $val }
195             unless ref($val) eq 'HASH';
196              
197             my @cookie = (
198             URI::Escape::uri_escape($name)
199             . '='
200             . URI::Escape::uri_escape($val->{value})
201 23         69 );
202              
203             push @cookie, 'domain=' . $val->{domain}
204 23 100       585 if defined($val->{domain});
205             push @cookie, 'path=' . $val->{path}
206 23 100       62 if defined($val->{path});
207             push @cookie, 'expires=' . $self->_date($val->{expires})
208 23 100       72 if defined($val->{expires});
209             push @cookie, 'max-age=' . $val->{'max-age'}
210 23 100       68 if defined($val->{'max-age'});
211             push @cookie, 'secure'
212 23 50       53 if $val->{secure};
213             push @cookie, 'HttpOnly'
214 23 50       53 if $val->{httponly};
215              
216 23         91 return join '; ', @cookie;
217             }
218              
219             # XXX DateTime?
220             my @MON = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
221             my @WDAY = qw( Sun Mon Tue Wed Thu Fri Sat );
222              
223             sub _date {
224 9     9   17 my $self = shift;
225 9         20 my ($expires) = @_;
226              
227 9 100       47 return $expires unless $expires =~ /^\d+$/;
228              
229 7         63 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($expires);
230 7         18 $year += 1900;
231              
232 7         49 return sprintf("%s, %02d-%s-%04d %02d:%02d:%02d GMT",
233             $WDAY[$wday], $mday, $MON[$mon], $year, $hour, $min, $sec);
234             }
235              
236             __PACKAGE__->meta->make_immutable;
237 20     20   216 no Moose;
  20         45  
  20         165  
238              
239              
240              
241             1;
242              
243             __END__
244              
245             =pod
246              
247             =head1 NAME
248              
249             Web::Response - common response class for web frameworks
250              
251             =head1 VERSION
252              
253             version 0.11
254              
255             =head1 SYNOPSIS
256              
257             use Web::Request;
258              
259             my $app = sub {
260             my ($env) = @_;
261             my $req = Web::Request->new_from_env($env);
262             # ...
263             return $req->new_response(status => 404)->finalize;
264             };
265              
266             =head1 DESCRIPTION
267              
268             Web::Response is a response class for L<PSGI> applications. Generally, you will
269             want to create instances of this class via C<new_response> on the request
270             object, since that allows a framework which subclasses L<Web::Request> to also
271             return an appropriate subclass of Web::Response.
272              
273             All attributes on Web::Response objects are writable, and the final state of
274             them will be used to generate a real L<PSGI> response when C<finalize> is
275             called.
276              
277             =head1 METHODS
278              
279             =head2 status($status)
280              
281             Sets (and returns) the status attribute, as described above.
282              
283             =head2 headers($headers)
284              
285             Sets (and returns) the headers attribute, as described above.
286              
287             =head2 header($name, $val)
288              
289             Shortcut for C<< $ret->headers->header($name, $val) >>.
290              
291             =head2 content_length($length)
292              
293             Shortcut for C<< $ret->headers->content_length($length) >>.
294              
295             =head2 content_type($type)
296              
297             Shortcut for C<< $ret->headers->content_type($type) >>.
298              
299             =head2 content_encoding($encoding)
300              
301             Shortcut for C<< $ret->headers->content_encoding($encoding) >>.
302              
303             =head2 location($location)
304              
305             Shortcut for C<< $ret->headers->header('Location', $location) >>.
306              
307             =head2 content($content)
308              
309             Sets (and returns) the C<content> attribute, as described above.
310              
311             =head2 streaming_response
312              
313             Sets and returns the streaming response coderef, as described above.
314              
315             =head2 has_streaming_response
316              
317             Returns whether or not a streaming response was provided.
318              
319             =head2 cookies($cookies)
320              
321             Sets (and returns) the C<cookies> attribute, as described above.
322              
323             =head2 has_cookies
324              
325             Returns whether or not any cookies have been defined.
326              
327             =head2 redirect($location, $status)
328              
329             Sets the C<Location> header to $location, and sets the status code to $status
330             (defaulting to 302 if not given).
331              
332             =head2 finalize
333              
334             Returns a valid L<PSGI> response, based on the values given. This can be either
335             an arrayref or a coderef, depending on if an immediate or streaming response
336             was provided. If both were provided, the streaming response will be preferred.
337              
338             =head2 to_app
339              
340             Returns a PSGI application which just returns the response in this object
341             directly.
342              
343             =head1 CONSTRUCTOR
344              
345             =head2 new(%params)
346              
347             Returns a new Web::Response object. Valid parameters are:
348              
349             =over 4
350              
351             =item status
352              
353             The HTTP status code for the response.
354              
355             =item headers
356              
357             The headers to return with the response. Can be provided as an arrayref, a
358             hashref, or an L<HTTP::Headers> object. Defaults to an L<HTTP::Headers> object
359             with no contents.
360              
361             =item content
362              
363             The content of the request. Can be provided as a string, an object which
364             overloads C<"">, an arrayref containing a list of either of those, a
365             filehandle, or an object that implements the C<getline> and C<close> methods.
366             Defaults to C<[]>.
367              
368             =item streaming_response
369              
370             Instead of C<status>/C<headers>/C<content>, you can provide a coderef which
371             implements the streaming response API described in the L<PSGI> specification.
372              
373             =item cookies
374              
375             A hashref of cookies to return with the response. The values in the hashref can
376             either be the string values of the cookies, or a hashref whose keys can be any
377             of C<value>, C<domain>, C<path>, C<expires>, C<max-age>, C<secure>,
378             C<httponly>. In addition to the date format that C<expires> normally uses,
379             C<expires> can also be provided as a UNIX timestamp (an epoch time, as returned
380             from C<time>). Defaults to C<{}>.
381              
382             =back
383              
384             In addition, a single parameter which is a valid PSGI response (a three element
385             arrayref or a coderef) will also be accepted, and will populate the attributes
386             as appropriate. If an arrayref is passed, the first element will be stored as
387             the C<status> attribute, the second element if it exists will be interpreted as
388             in the PSGI specification to create an L<HTTP::Headers> object and stored in
389             the C<headers> attribute, and the third element if it exists will be stored as
390             the C<content> attribute. If a coderef is passed, it will be stored in the
391             C<streaming_response> attribute.
392              
393             =head1 AUTHOR
394              
395             Jesse Luehrs <doy@tozt.net>
396              
397             =head1 COPYRIGHT AND LICENSE
398              
399             This software is copyright (c) 2013 by Jesse Luehrs.
400              
401             This is free software; you can redistribute it and/or modify it under
402             the same terms as the Perl 5 programming language system itself.
403              
404             =cut