File Coverage

blib/lib/Dancer2/Core/Response.pm
Criterion Covered Total %
statement 94 95 98.9
branch 22 26 84.6
condition 9 14 64.2
subroutine 27 27 100.0
pod 11 13 84.6
total 163 175 93.1


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 = '0.400001';
5 142     142   83387 use Moo;
  142         7292  
  142         991  
6              
7 142     142   50378 use Encode;
  142         30789  
  142         13514  
8 142     142   1427 use Dancer2::Core::Types;
  142         350  
  142         1111  
9              
10 142     142   1770148 use Dancer2 ();
  142         411  
  142         3213  
11 142     142   847 use Dancer2::Core::HTTP;
  142         386  
  142         4075  
12              
13 142     142   884 use HTTP::Headers::Fast;
  142         371  
  142         5221  
14 142     142   1050 use Scalar::Util qw(blessed);
  142         363  
  142         8863  
15 142     142   1000 use Plack::Util;
  142         383  
  142         3975  
16 142     142   857 use Safe::Isa;
  142         392  
  142         19732  
17 142     142   1121 use Sub::Quote ();
  142         409  
  142         11436  
18              
19             use overload
20 28     28   66 '@{}' => sub { $_[0]->to_psgi },
21 142     142   1110 '""' => sub { $_[0] };
  142     1615   403  
  142         1989  
  1615         20709  
22              
23             has headers => (
24             is => 'ro',
25             isa => InstanceOf['HTTP::Headers'],
26             lazy => 1,
27             coerce => sub {
28             my ($value) = @_;
29             # HTTP::Headers::Fast reports that it isa 'HTTP::Headers',
30             # but there is no actual inheritance.
31             $value->$_isa('HTTP::Headers')
32             ? $value
33             : HTTP::Headers::Fast->new(@{$value});
34             },
35             default => sub {
36             HTTP::Headers::Fast->new();
37             },
38             handles => [qw<header push_header>],
39             );
40              
41             sub headers_to_array {
42 626     626 1 7518 my $self = shift;
43 626   66     2014 my $headers = shift || $self->headers;
44              
45 626         1304 my @hdrs;
46             $headers->scan( sub {
47 1963     1963   32043 my ( $k, $v ) = @_;
48 1963         3667 $v =~ s/\015\012[\040|\011]+/chr(32)/ge; # replace LWS with a single SP
  0         0  
49 1963         4686 $v =~ s/\015|\012//g; # remove CR and LF since the char is invalid here
50 1963         5300 push @hdrs, $k => $v;
51 626         6639 });
52              
53 626         5393 return \@hdrs;
54             }
55              
56             # boolean to tell if the route passes or not
57             has has_passed => (
58             is => 'rw',
59             isa => Bool,
60             default => sub {0},
61             );
62              
63 2     2 1 51 sub pass { shift->has_passed(1) }
64              
65             has serializer => (
66             is => 'ro',
67             isa => ConsumerOf ['Dancer2::Core::Role::Serializer'],
68             );
69              
70             has is_encoded => (
71             is => 'rw',
72             isa => Bool,
73             default => sub {0},
74             );
75              
76             has is_halted => (
77             is => 'rw',
78             isa => Bool,
79             default => sub {0},
80             );
81              
82             sub halt {
83 8     8 1 92 my ( $self, $content ) = @_;
84 8 100       70 $self->content( $content ) if @_ > 1;
85 8         541 $self->is_halted(1);
86             }
87              
88             has status => (
89             is => 'rw',
90             isa => Num,
91             default => sub {200},
92             lazy => 1,
93             coerce => sub { Dancer2::Core::HTTP->status(shift) },
94             );
95              
96             has content => (
97             is => 'rw',
98             isa => Str,
99             predicate => 'has_content',
100             clearer => 'clear_content',
101             );
102              
103             has server_tokens => (
104             is => 'ro',
105             isa => Bool,
106             default => sub {1},
107             );
108              
109             around content => sub {
110             my ( $orig, $self ) = ( shift, shift );
111              
112             # called as getter?
113             @_ or return $self->$orig;
114              
115             # No serializer defined; encode content
116             $self->serializer
117             or return $self->$orig( $self->encode_content(@_) );
118              
119             # serialize content
120             my $serialized = $self->serialize(@_);
121             $self->is_encoded(1); # All serializers return byte strings
122             return $self->$orig( defined $serialized ? $serialized : '' );
123             };
124              
125             has default_content_type => (
126             is => 'rw',
127             isa => Str,
128             default => sub {'text/html'},
129             );
130              
131             sub encode_content {
132 536     536 1 1328 my ( $self, $content ) = @_;
133              
134 536 100       8686 return $content if $self->is_encoded;
135              
136             # Apply default content type if none set.
137 529   66     6427 my $ct = $self->content_type ||
138             $self->content_type( $self->default_content_type );
139              
140 529 50       8359 return $content if $ct !~ /^text/;
141              
142             # we don't want to encode an empty string, it will break the output
143 529 100       1987 $content or return $content;
144              
145 494 50       6807 $self->content_type("$ct; charset=UTF-8")
146             if $ct !~ /charset/;
147              
148 494         8435 $self->is_encoded(1);
149 494         16793 return Encode::encode( 'UTF-8', $content );
150             }
151              
152             sub new_from_plack {
153 1     1 1 4236 my ($self, $psgi_res) = @_;
154              
155 1         4 return Dancer2::Core::Response->new(
156             status => $psgi_res->status,
157             headers => $psgi_res->headers,
158             content => $psgi_res->body,
159             );
160             }
161              
162             sub new_from_array {
163 1     1 1 3069 my ($self, $arrayref) = @_;
164              
165 1         8 return Dancer2::Core::Response->new(
166             status => $arrayref->[0],
167             headers => $arrayref->[1],
168             content => $arrayref->[2][0],
169             );
170             }
171              
172             sub to_psgi {
173 607     607 1 13246 my ($self) = @_;
174              
175 607 100       4527 $self->server_tokens
176             and $self->header( 'Server' => "Perl Dancer2 " . Dancer2->VERSION );
177              
178 607         44240 my $headers = $self->headers;
179 607         13477 my $status = $self->status;
180              
181 607 100       16367 Plack::Util::status_with_no_entity_body($status)
182             and return [ $status, $self->headers_to_array($headers), [] ];
183              
184 606         15024 my $content = $self->content;
185             # It is possible to have no content and/or no content type set
186             # e.g. if all routes 'pass'. Set the default value for the content
187             # (an empty string), allowing serializer hooks to be triggered
188             # as they may change the content..
189 606 100       5247 $content = $self->content('') if ! defined $content;
190              
191 606 100 66     4360 if ( !$headers->header('Content-Length') &&
      66        
192             !$headers->header('Transfer-Encoding') &&
193             defined( my $content_length = length $content ) ) {
194 579         32227 $headers->push_header( 'Content-Length' => $content_length );
195             }
196              
197             # More defaults
198 606 100       15007 $self->content_type or $self->content_type($self->default_content_type);
199 606         30506 return [ $status, $self->headers_to_array($headers), [ $content ], ];
200             }
201              
202             # sugar for accessing the content_type header, with mimetype care
203             sub content_type {
204 2249     2249 1 59899 my $self = shift;
205              
206 2249 100       4986 if ( scalar @_ > 0 ) {
207 1112         3358 my $runner = Dancer2::runner();
208 1112         5178 my $mimetype = $runner->mime_type->name_or_type(shift);
209 1112         19795 $self->header( 'Content-Type' => $mimetype );
210 1112         73940 return $mimetype;
211             }
212             else {
213 1137         19092 return $self->header('Content-Type');
214             }
215             }
216              
217             has _forward => (
218             is => 'rw',
219             isa => HashRef,
220             );
221              
222             sub forward {
223 1     1 0 427 my ( $self, $uri, $params, $opts ) = @_;
224 1         28 $self->_forward( { to_url => $uri, params => $params, options => $opts } );
225             }
226              
227             sub is_forwarded {
228 2     2 0 835 my $self = shift;
229 2         46 $self->_forward;
230             }
231              
232             sub redirect {
233 26     26 1 217 my ( $self, $destination, $status ) = @_;
234 26   50     456 $self->status( $status || 302 );
235              
236             # we want to stringify the $destination object (URI object)
237 26         1086 $self->header( 'Location' => "$destination" );
238             }
239              
240             sub error {
241 1     1 1 33 my $self = shift;
242              
243 1         22 my $error = Dancer2::Core::Error->new(
244             response => $self,
245             @_,
246             );
247              
248 1         5 $error->throw;
249 1         16 return $error;
250             }
251              
252             sub serialize {
253 77     77 1 206 my ($self, $content) = @_;
254              
255 77 50       252 my $serializer = $self->serializer
256             or return;
257              
258 77 50       1475 $content = $serializer->serialize($content)
259             or return;
260              
261 77         444 $self->content_type( $serializer->content_type );
262 77         228 return $content;
263             }
264              
265             1;
266              
267             __END__
268              
269             =pod
270              
271             =encoding UTF-8
272              
273             =head1 NAME
274              
275             Dancer2::Core::Response - Response object for Dancer2
276              
277             =head1 VERSION
278              
279             version 0.400001
280              
281             =head1 ATTRIBUTES
282              
283             =head2 is_encoded
284              
285             Flag to tell if the content has already been encoded.
286              
287             =head2 is_halted
288              
289             Flag to tell whether or not the response should continue to be processed.
290              
291             =head2 status
292              
293             The HTTP status for the response.
294              
295             =head2 content
296              
297             The content for the response, stored as a string. If a reference is passed, the
298             response will try coerce it to a string via double quote interpolation.
299              
300             =head2 default_content_type
301              
302             Default mime type to use for the response Content-Type header
303             if nothing was specified
304              
305             =head2 headers
306              
307             The attribute that store the headers in a L<HTTP::Headers::Fast> object.
308              
309             That attribute coerces from ArrayRef and defaults to an empty L<HTTP::Headers::Fast>
310             instance.
311              
312             =head1 METHODS
313              
314             =head2 pass
315              
316             Set has_passed to true.
317              
318             =head2 serializer()
319              
320             Returns the optional serializer object used to deserialize request parameters
321              
322             =head2 halt
323              
324             Shortcut to halt the current response by setting the is_halted flag.
325              
326             =head2 encode_content
327              
328             Encodes the stored content according to the stored L<content_type>. If the content_type
329             is a text format C<^text>, then no encoding will take place.
330              
331             Internally, it uses the L<is_encoded> flag to make sure that content is not encoded twice.
332              
333             If it encodes the content, then it will return the encoded content. In all other
334             cases it returns C<false>.
335              
336             =head2 new_from_plack
337              
338             Creates a new response object from a L<Plack::Response> object.
339              
340             =head2 new_from_array
341              
342             Creates a new response object from a PSGI arrayref.
343              
344             =head2 to_psgi
345              
346             Converts the response object to a PSGI array.
347              
348             =head2 content_type($type)
349              
350             A little sugar for setting or accessing the content_type of the response, via the headers.
351              
352             =head2 redirect ($destination, $status)
353              
354             Sets a header in this response to give a redirect to $destination, and sets the
355             status to $status. If $status is omitted, or false, then it defaults to a status of
356             302.
357              
358             =head2 error( @args )
359              
360             $response->error( message => "oops" );
361              
362             Creates a L<Dancer2::Core::Error> object with the given I<@args> and I<throw()>
363             it against the response object. Returns the error object.
364              
365             =head2 serialize( $content )
366              
367             $response->serialize( $content );
368              
369             Serialize and return $content with the response's serializer.
370             set content-type accordingly.
371              
372             =head2 header($name)
373              
374             Return the value of the given header, if present. If the header has multiple
375             values, returns the list of values if called in list context, the first one
376             if in scalar context.
377              
378             =head2 push_header
379              
380             Add the header no matter if it already exists or not.
381              
382             $self->push_header( 'X-Wing' => '1' );
383              
384             It can also be called with multiple values to add many times the same header
385             with different values:
386              
387             $self->push_header( 'X-Wing' => 1, 2, 3 );
388              
389             =head2 headers_to_array($headers)
390              
391             Convert the C<$headers> to a PSGI ArrayRef.
392              
393             If no C<$headers> are provided, it will use the current response headers.
394              
395             =head1 AUTHOR
396              
397             Dancer Core Developers
398              
399             =head1 COPYRIGHT AND LICENSE
400              
401             This software is copyright (c) 2023 by Alexis Sukrieh.
402              
403             This is free software; you can redistribute it and/or modify it under
404             the same terms as the Perl 5 programming language system itself.
405              
406             =cut