File Coverage

blib/lib/Prancer/Response.pm
Criterion Covered Total %
statement 85 90 94.4
branch 21 30 70.0
condition 4 6 66.6
subroutine 18 18 100.0
pod 6 7 85.7
total 134 151 88.7


line stmt bran cond sub pod time code
1             package Prancer::Response;
2              
3 3     3   817 use strict;
  3         4  
  3         106  
4 3     3   15 use warnings FATAL => 'all';
  3         6  
  3         184  
5              
6 3     3   14 use version;
  3         4  
  3         15  
7             our $VERSION = '1.02';
8              
9 3     3   1476 use Plack::Response;
  3         4969  
  3         84  
10 3     3   22 use Hash::MultiValue;
  3         4  
  3         55  
11 3     3   13 use URI::Escape ();
  3         3  
  3         37  
12 3     3   10 use HTTP::Headers;
  3         4  
  3         40  
13 3     3   9 use Carp;
  3         3  
  3         2891  
14              
15             # even though this *should* work automatically, it was not
16             our @CARP_NOT = qw(Prancer Try::Tiny);
17              
18             sub new {
19 6     6 0 43 my ($class, $env) = @_;
20 6         34 return bless({
21             '_response' => Plack::Response->new($env),
22             '_cookies' => Hash::MultiValue->new(),
23             '_headers' => Hash::MultiValue->new(),
24             }, $class);
25             }
26              
27             # set a single header
28             # or get all the keys
29             sub header {
30 3     3 1 69 my $self = shift;
31              
32             # if we are given multiple args assume they are headers in key/value pairs
33 3 50       24 croak "odd number of headers" unless (@_ % 2 == 0);
34 3         7 while (@_) {
35 4         43 my ($key, $value) = (shift(@_), shift(@_));
36 4 50       9 $self->headers->add($key => [@{$self->headers->get_all($key) || []}, $value]);
  4         9  
37             }
38              
39 3         132 return;
40             }
41              
42             # get all the headers that have been set
43             sub headers {
44 18     18 1 23 my $self = shift;
45 18         62 return $self->{'_headers'};
46             }
47              
48             # set a single cookie
49             # or get all the keys
50             sub cookie {
51 3     3 1 4 my $self = shift;
52              
53             # return the keys if nothing is asked for
54 3 50       9 return keys(%{$self->cookies()}) unless @_;
  0         0  
55              
56             # if given just a key then return that
57 3 50       8 if (@_ == 1) {
58 0         0 my $key = shift;
59 0 0       0 return $self->cookies->{$key} unless wantarray;
60 0         0 return $self->cookies->get_all($key);
61             }
62              
63             # if we are given multiple args assume they are cookies in key/value pairs
64 3 50       9 croak "odd number of cookies" unless (@_ % 2 == 0);
65 3         15 while (@_) {
66 3         6 my ($key, $value) = (shift(@_), shift(@_));
67              
68             # take a moment to validate the cookie
69             # TODO
70              
71 3 50       8 $self->cookies->add($key => [@{$self->cookies->get_all($key) || []}, $value]);
  3         6  
72             }
73              
74 3         112 return;
75             }
76              
77             sub cookies {
78 15     15 1 20 my $self = shift;
79 15         55 return $self->{'_cookies'};
80             }
81              
82             sub body {
83 2     2 1 92 my $self = shift;
84              
85             # make the response be a callback
86 2 100 66     17 if (ref($_[0]) && ref($_[0]) eq "CODE") {
87 1         2 $self->{'_callback'} = shift;
88 1         2 return;
89             }
90              
91             # just add this to the body, whatever it is
92 1         5 return $self->{'_response'}->body(@_);
93             }
94              
95             sub finalize {
96 6     6 1 974 my ($self, $status) = @_;
97 6         30 $self->{'_response'}->status($status);
98              
99             # build the headers using something normal and then add them to the
100             # response later. for whatever reason plack is being weird about this when
101             # the same header name is being used more than once. though, i might be
102             # doing it wrong.
103 6         54 my $headers = HTTP::Headers->new();
104              
105             # add normal headers
106 6         40 for my $key (keys %{$self->headers()}) {
  6         19  
107 3         65 for my $value (@{$self->headers->get_all($key)}) {
  3         6  
108 3         42 $headers->push_header($key => $value);
109             }
110             }
111              
112             # add cookies
113 6         28 for my $key (keys %{$self->cookies()}) {
  6         19  
114 3         67 for my $value (@{$self->cookies->get_all($key)}) {
  3         8  
115 3         41 $headers->push_header("Set-Cookie" => $self->_bake_cookie($key, $value));
116             }
117             }
118              
119             # now add the headers we've compiled
120 6         45 $self->{'_response'}->headers($headers);
121              
122 6 100 66     77 if (ref($self->{'_callback'}) &&
123             ref($self->{'_callback'}) eq "CODE") {
124              
125             # the extra array ref brackets around the sub are because Web::Simple,
126             # which we use as the router, will not do a callback without them. by
127             # returning an array ref we are telling Web::Simple that we are giving
128             # it a PSGI response. from the Web::Simple docs:
129             #
130             # Well, a sub is a valid PSGI response too (for ultimate streaming
131             # and async cleverness). If you want to return a PSGI sub you have
132             # to wrap it into an array ref.
133             #
134             return [ sub {
135 1     1   1022 my $responder = shift;
136              
137             # this idiom here borrows heavily from the documentation on this
138             # blog post, by tatsuhiko miyagawa:
139             #
140             # http://bulknews.typepad.com/blog/2009/10/psgiplack-streaming-is-now-complete.html
141             #
142             # this effectively allows the user of this api to stream data to
143             # the client.
144              
145             # finalize will always return a three element array. the third
146             # element is supposed to be the body. because we don't have a body
147             # yet (it's in the callback), this uses splice to exclude the third
148             # element (aka the body) and just return the status code and the
149             # list of headers.
150 1         3 my $writer = $responder->([splice(@{$self->{'_response'}->finalize()}, 0, 2)]);
  1         6  
151 1         102 return $self->{'_callback'}->($writer);
152 1         5 } ];
153             }
154              
155             # just return a normal response
156 5         21 return $self->{'_response'}->finalize();
157             }
158              
159             sub _bake_cookie {
160 3     3   5 my ($self, $key, $value) = @_;
161              
162 3         9 my @cookie = (URI::Escape::uri_escape($key) . "=" . URI::Escape::uri_escape($value->{'value'}));
163 3 100       104 push(@cookie, "domain=" . $value->{'domain'}) if $value->{'domain'};
164 3 100       12 push(@cookie, "path=" . $value->{'path'}) if $value->{'path'};
165 3 100       12 push(@cookie, "expires=" . $self->_cookie_date($value->{'expires'})) if $value->{'expires'};
166 3 100       17 push(@cookie, "secure") if $value->{'secure'};
167 3 100       9 push(@cookie, "HttpOnly") if $value->{'httponly'};
168 3         22 return join("; ", @cookie);
169              
170             }
171              
172             my @MON = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
173             my @WDAY = qw( Sun Mon Tue Wed Thu Fri Sat );
174              
175             sub _cookie_date {
176 2     2   4 my ($self, $expires) = @_;
177              
178 2 50       16 if ($expires =~ /^\-?\d+$/x) {
179             # all numbers -> epoch date
180             # (cookies use '-' as date separator, HTTP uses ' ')
181 2         24 my ($sec, $min, $hour, $mday, $mon, $year, $wday) = gmtime($expires);
182 2         5 $year += 1900;
183              
184 2         20 return sprintf("%s, %02d-%s-%04d %02d:%02d:%02d GMT",
185             $WDAY[$wday], $mday, $MON[$mon], $year, $hour, $min, $sec);
186             }
187              
188 0           return $expires;
189             }
190              
191             1;
192              
193             =head1 NAME
194              
195             Prancer::Response
196              
197             =head1 SYNOPSIS
198              
199             sub handler {
200             my ($self, $env, $request, $response, $session) = @_;
201              
202             ...
203              
204             sub (GET) {
205             $response->header("Content-Type" => "text/plain");
206             $response->body("hello, goodbye");
207             return $response->finalize(200);
208             }
209             }
210              
211             # or using a callback
212             sub handler {
213              
214             ...
215              
216             sub (GET) {
217             $response->header("Content-Type" => "text/plain");
218             $response->body(sub {
219             my $writer = shift;
220             $writer->write("What is up?");
221             $writer->close();
222             });
223             return $response->finalize(200);
224             }
225             }
226              
227             =head1 METHODS
228              
229             =over
230              
231             =item header
232              
233             This method expects a list of headers to add to the response. For example:
234              
235             $response->header("Content-Type" => "text/plain");
236             $response->header("Content-Length" => 1234, "X-Foo" => "bar");
237              
238             If the header has already been set this will add another value to it and the
239             response will include the same header multiple times. To replace a header that
240             has already been set, remove the existing value first:
241              
242             $response->headers->remove("X-Foo");
243              
244             =item headers
245              
246             Returns a L of all headers that have been set to be sent with
247             the response.
248              
249             =item cookie
250              
251             If called with no arguments this will return the names of all cookies that have
252             been set to be sent with the response. Otherwise, this method expects a list of
253             cookies to add to the response. For example:
254              
255             $response->cookie("foo" => {
256             'value' => "test",
257             'path' => "/",
258             'domain' => ".example.com",
259             'expires' => time + 24 * 60 * 60,
260             });
261              
262             The hashref may contain the keys C, C, C, C,
263             C, and C. C can take a string or an integer (as an
264             epoch time) and B convert string formats like C<+3M>.
265              
266             =item cookies
267              
268             Returns a L of all cookies that have been set to be sent with
269             the response.
270              
271             =item body
272              
273             Send buffered output to the client. Anything sent to the client with this
274             method will be buffered until C is called. For example:
275              
276             $response->body("hello");
277             $response->body("goodbye", "world");
278              
279             If a buffered response is not desired then the body may be a callback to send a
280             streaming response to the client. Any headers or response codes set in the
281             callback will be ignored as they must all be set beforehand. Any body set
282             before a callback is set will also be ignored. For example:
283              
284             $response->body(sub {
285             my $writer = shift;
286             $writer->write("Hello, world!");
287             $writer->close();
288             return;
289             });
290              
291             =item finalize
292              
293             This requires one argument: the HTTP status code of the response. It will then
294             send a PSGI compatible response to the client. For example:
295              
296             # or hard code it
297             $response->finalize(200);
298              
299             =back
300              
301             =cut