File Coverage

blib/lib/Dancer2/Core/Response.pm
Criterion Covered Total %
statement 102 111 91.8
branch 26 36 72.2
condition 13 20 65.0
subroutine 29 29 100.0
pod 11 13 84.6
total 181 209 86.6


line stmt bran cond sub pod time code
1             # ABSTRACT: Response object for Dancer2
2              
3             package Dancer2::Core::Response;
4             $Dancer2::Core::Response::VERSION = '2.1.0';
5 155     155   492824 use Moo;
  155         20907  
  155         1355  
6              
7 155     155   74200 use Carp;
  155         442  
  155         14941  
8 155     155   2126 use Encode;
  155         33476  
  155         17502  
9 155     155   2380 use Dancer2::Core::Types;
  155         433  
  155         1633  
10 155     155   2339441 use Dancer2::Core::MIME;
  155         426  
  155         5283  
11              
12 155     155   8366 use Dancer2 ();
  155         479  
  155         4177  
13 155     155   1034 use Dancer2::Core::HTTP;
  155         434  
  155         5635  
14              
15 155     155   9143 use HTTP::Headers::Fast;
  155         3916  
  155         9832  
16 155     155   2323 use Scalar::Util qw(blessed);
  155         7538  
  155         14810  
17 155     155   2689 use Plack::Util;
  155         788  
  155         5387  
18 155     155   1285 use Safe::Isa;
  155         421  
  155         25215  
19 155     155   1106 use Sub::Quote ();
  155         346  
  155         12899  
20              
21             use overload
22 24     24   111 '@{}' => sub { $_[0]->to_psgi },
23 155     155   979 '""' => sub { $_[0] };
  155     1735   375  
  155         2265  
  1735         26219  
24              
25             my $WARNED_NO_CHARSET = 0;
26              
27             has mime_type => (
28             'is' => 'ro',
29             'isa' => InstanceOf['Dancer2::Core::MIME'],
30             'default' => sub { Dancer2::Core::MIME->new() },
31             );
32              
33             has headers => (
34             is => 'ro',
35             isa => InstanceOf['HTTP::Headers'],
36             lazy => 1,
37             coerce => sub {
38             my ($value) = @_;
39             # HTTP::Headers::Fast reports that it isa 'HTTP::Headers',
40             # but there is no actual inheritance.
41             $value->$_isa('HTTP::Headers')
42             ? $value
43             : HTTP::Headers::Fast->new(@{$value});
44             },
45             default => sub {
46             HTTP::Headers::Fast->new();
47             },
48             handles => [qw
],
49             );
50              
51             has strict_utf8 => (
52             is => 'ro',
53             isa => Bool,
54             default => sub {0},
55             );
56              
57             has log_cb => (
58             is => 'ro',
59             isa => CodeRef,
60             predicate => 'has_log_cb',
61             );
62              
63             sub headers_to_array {
64 666     666 1 11115 my $self = shift;
65 666   66     2710 my $headers = shift || $self->headers;
66              
67 666         1628 my @hdrs;
68             $headers->scan( sub {
69 1437     1437   31281 my ( $k, $v ) = @_;
70 1437         3745 $v =~ s/\015\012[\040|\011]+/chr(32)/ge; # replace LWS with a single SP
  0         0  
71 1437         4572 $v =~ s/\015|\012//g; # remove CR and LF since the char is invalid here
72 1437         4607 push @hdrs, $k => $v;
73 666         6509 });
74              
75 666         7647 return \@hdrs;
76             }
77              
78             # boolean to tell if the route passes or not
79             has has_passed => (
80             is => 'rw',
81             isa => Bool,
82             default => sub {0},
83             );
84              
85 2     2 1 68 sub pass { shift->has_passed(1) }
86              
87             has serializer => (
88             is => 'rw',
89             isa => ConsumerOf ['Dancer2::Core::Role::Serializer'],
90             );
91              
92             has charset => (
93             is => 'rw',
94             isa => Str,
95             predicate => 'has_charset',
96             );
97              
98             has is_encoded => (
99             is => 'rw',
100             isa => Bool,
101             default => sub {0},
102             );
103              
104             has is_halted => (
105             is => 'rw',
106             isa => Bool,
107             default => sub {0},
108             );
109              
110             sub halt {
111 10     10 1 176 my ( $self, $content ) = @_;
112 10 100       69 $self->content( $content ) if @_ > 1;
113 10         447 $self->is_halted(1);
114             }
115              
116             has status => (
117             is => 'rw',
118             isa => Num,
119             default => sub {200},
120             lazy => 1,
121             coerce => sub { Dancer2::Core::HTTP->status(shift) },
122             );
123              
124             has content => (
125             is => 'rw',
126             isa => Str,
127             predicate => 'has_content',
128             clearer => 'clear_content',
129             );
130              
131             has server_tokens => (
132             is => 'ro',
133             isa => Bool,
134             default => sub {1},
135             );
136              
137             around content => sub {
138             my ( $orig, $self ) = ( shift, shift );
139              
140             # called as getter?
141             @_ or return $self->$orig;
142              
143             # No serializer defined; encode content
144             $self->serializer
145             or return $self->$orig( $self->encode_content(@_) );
146              
147             # serialize content
148             my $serialized = $self->serialize(@_);
149             $self->is_encoded(1); # All serializers return byte strings
150             return $self->$orig( defined $serialized ? $serialized : '' );
151             };
152              
153             has default_content_type => (
154             is => 'rw',
155             isa => Str,
156             default => sub {'text/html'},
157             );
158              
159             sub encode_content {
160 577     577 1 7271 my ( $self, $content ) = @_;
161              
162 577 100       12765 return $content if $self->is_encoded;
163              
164             # Apply default content type if none set.
165 568   66     5541 my $ct = $self->content_type ||
166             $self->content_type( $self->default_content_type );
167              
168 568 50       10799 return $content if $ct !~ /^text/;
169 568         12738 my $charset = $self->headers->content_type_charset;
170 568 100 66     61889 $charset = $self->charset
171             if !defined $charset && $self->has_charset;
172 568 100 66     6686 if ( !defined $charset || $charset eq '' ) {
173 1 50       34 return $content if !utf8::is_utf8($content);
174 0         0 my $msg = 'Response contains characters but no charset is configured; assuming UTF-8';
175 0 0       0 $self->strict_utf8
176             and Carp::croak($msg);
177 0 0       0 if ( !$WARNED_NO_CHARSET ) {
178 0         0 $WARNED_NO_CHARSET = 1;
179 0 0       0 if ( $self->has_log_cb ) {
180 0         0 $self->log_cb->( warning => $msg );
181             }
182             else {
183 0         0 Carp::carp($msg);
184             }
185             }
186 0         0 $charset = 'UTF-8';
187             }
188              
189             # we don't want to encode an empty string, it will break the output
190 567 100       2369 $content or return $content;
191              
192 533 50       3888 $self->content_type("$ct; charset=$charset")
193             if $ct !~ /charset/;
194              
195 533         11844 $self->is_encoded(1);
196 533         22684 return Encode::encode( $charset, $content );
197             }
198              
199             sub new_from_plack {
200 1     1 1 8204 my ($class, $psgi_res) = @_;
201              
202 1         4 return Dancer2::Core::Response->new(
203             status => $psgi_res->status,
204             headers => $psgi_res->headers,
205             content => $psgi_res->body,
206             );
207             }
208              
209             sub new_from_array {
210 1     1 1 505686 my ($class, $arrayref) = @_;
211              
212 1         11 return Dancer2::Core::Response->new(
213             status => $arrayref->[0],
214             headers => $arrayref->[1],
215             content => $arrayref->[2][0],
216             );
217             }
218              
219             sub to_psgi {
220 644     644 1 17565 my ($self) = @_;
221              
222 644         15138 my $headers = $self->headers;
223 644         18126 my $status = $self->status;
224              
225 644 100       21407 Plack::Util::status_with_no_entity_body($status)
226             and return [ $status, $self->headers_to_array($headers), [] ];
227              
228 643         20783 my $content = $self->content;
229             # It is possible to have no content and/or no content type set
230             # e.g. if all routes 'pass'. Set the default value for the content
231             # (an empty string), allowing serializer hooks to be triggered
232             # as they may change the content..
233 643 100       6565 $content = $self->content('') if ! defined $content;
234              
235 643 100 66     3865 if ( !$headers->header('Content-Length') &&
      66        
236             !$headers->header('Transfer-Encoding') &&
237             defined( my $content_length = length $content ) ) {
238 620         40578 $headers->push_header( 'Content-Length' => $content_length );
239             }
240              
241             # More defaults
242 643 100       18806 $self->content_type or $self->content_type($self->default_content_type);
243 643         38681 return [ $status, $self->headers_to_array($headers), [ $content ], ];
244             }
245              
246             # sugar for accessing the content_type header, with mimetype care
247             sub content_type {
248 2405     2405 1 76554 my $self = shift;
249              
250 2405 100       6203 if ( scalar @_ > 0 ) {
251 1192         7122 my $mimetype = $self->mime_type->name_or_type(shift);
252 1192         26154 $self->header( 'Content-Type' => $mimetype );
253 1192         96291 return $mimetype;
254             }
255             else {
256 1213         29083 return $self->header('Content-Type');
257             }
258             }
259              
260             has _forward => (
261             is => 'rw',
262             isa => HashRef,
263             );
264              
265             sub forward {
266 1     1 0 420 my ( $self, $uri, $params, $opts ) = @_;
267 1         67 $self->_forward( { to_url => $uri, params => $params, options => $opts } );
268             }
269              
270             sub is_forwarded {
271 2     2 0 1145 my $self = shift;
272 2         66 $self->_forward;
273             }
274              
275             sub redirect {
276 26     26 1 245 my ( $self, $destination, $status ) = @_;
277 26   50     584 $self->status( $status || 302 );
278              
279             # we want to stringify the $destination object (URI object)
280 26         1305 $self->header( 'Location' => "$destination" );
281             }
282              
283             sub error {
284 1     1 1 36 my $self = shift;
285              
286 1         31 my $error = Dancer2::Core::Error->new(
287             response => $self,
288             @_,
289             );
290              
291 1         6 $error->throw;
292 1         15 return $error;
293             }
294              
295             sub serialize {
296 79     79 1 222 my ($self, $content) = @_;
297              
298 79 50       1449 my $serializer = $self->serializer
299             or return;
300              
301 79 100       2349 $content = $serializer->serialize($content)
302             or return;
303              
304 78         555 $self->content_type( $serializer->content_type );
305 78         244 return $content;
306             }
307              
308             1;
309              
310             __END__