File Coverage

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


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 = '1.0.0';
5 143     143   82499 use Moo;
  143         7811  
  143         1048  
6              
7 143     143   51991 use Encode;
  143         31529  
  143         14088  
8 143     143   1668 use Dancer2::Core::Types;
  143         466  
  143         1106  
9              
10 143     143   1841028 use Dancer2 ();
  143         488  
  143         3670  
11 143     143   989 use Dancer2::Core::HTTP;
  143         410  
  143         4446  
12              
13 143     143   968 use HTTP::Headers::Fast;
  143         437  
  143         6003  
14 143     143   1038 use Scalar::Util qw(blessed);
  143         407  
  143         8764  
15 143     143   1099 use Plack::Util;
  143         495  
  143         4849  
16 143     143   1072 use Safe::Isa;
  143         417  
  143         20920  
17 143     143   1245 use Sub::Quote ();
  143         484  
  143         13363  
18              
19             use overload
20 28     28   77 '@{}' => sub { $_[0]->to_psgi },
21 143     143   1187 '""' => sub { $_[0] };
  143     1635   420  
  143         2395  
  1635         21898  
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 636     636 1 7336 my $self = shift;
43 636   66     3598 my $headers = shift || $self->headers;
44              
45 636         1336 my @hdrs;
46             $headers->scan( sub {
47 1993     1993   33426 my ( $k, $v ) = @_;
48 1993         3865 $v =~ s/\015\012[\040|\011]+/chr(32)/ge; # replace LWS with a single SP
  0         0  
49 1993         4834 $v =~ s/\015|\012//g; # remove CR and LF since the char is invalid here
50 1993         5616 push @hdrs, $k => $v;
51 636         4738 });
52              
53 636         5763 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 53 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 99 my ( $self, $content ) = @_;
84 8 100       70 $self->content( $content ) if @_ > 1;
85 8         521 $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 545     545 1 1388 my ( $self, $content ) = @_;
133              
134 545 100       9140 return $content if $self->is_encoded;
135              
136             # Apply default content type if none set.
137 538   66     5098 my $ct = $self->content_type ||
138             $self->content_type( $self->default_content_type );
139              
140 538 50       8933 return $content if $ct !~ /^text/;
141              
142             # we don't want to encode an empty string, it will break the output
143 538 100       2060 $content or return $content;
144              
145 503 50       6540 $self->content_type("$ct; charset=UTF-8")
146             if $ct !~ /charset/;
147              
148 503         9029 $self->is_encoded(1);
149 503         16831 return Encode::encode( 'UTF-8', $content );
150             }
151              
152             sub new_from_plack {
153 1     1 1 4197 my ($self, $psgi_res) = @_;
154              
155 1         6 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 3063 my ($self, $arrayref) = @_;
164              
165 1         10 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 617     617 1 13571 my ($self) = @_;
174              
175 617 100       4583 $self->server_tokens
176             and $self->header( 'Server' => "Perl Dancer2 " . Dancer2->VERSION );
177              
178 617         46983 my $headers = $self->headers;
179 617         14487 my $status = $self->status;
180              
181 617 100       16890 Plack::Util::status_with_no_entity_body($status)
182             and return [ $status, $self->headers_to_array($headers), [] ];
183              
184 616         17904 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 616 100       5841 $content = $self->content('') if ! defined $content;
190              
191 616 100 66     2893 if ( !$headers->header('Content-Length') &&
      66        
192             !$headers->header('Transfer-Encoding') &&
193             defined( my $content_length = length $content ) ) {
194 589         40308 $headers->push_header( 'Content-Length' => $content_length );
195             }
196              
197             # More defaults
198 616 100       15931 $self->content_type or $self->content_type($self->default_content_type);
199 616         32387 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 2287     2287 1 62869 my $self = shift;
205              
206 2287 100       9454 if ( scalar @_ > 0 ) {
207 1131         3381 my $runner = Dancer2::runner();
208 1131         6938 my $mimetype = $runner->mime_type->name_or_type(shift);
209 1131         20964 $self->header( 'Content-Type' => $mimetype );
210 1131         78176 return $mimetype;
211             }
212             else {
213 1156         20170 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 423 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 739 my $self = shift;
229 2         41 $self->_forward;
230             }
231              
232             sub redirect {
233 26     26 1 238 my ( $self, $destination, $status ) = @_;
234 26   50     501 $self->status( $status || 302 );
235              
236             # we want to stringify the $destination object (URI object)
237 26         1168 $self->header( 'Location' => "$destination" );
238             }
239              
240             sub error {
241 1     1 1 25 my $self = shift;
242              
243 1         20 my $error = Dancer2::Core::Error->new(
244             response => $self,
245             @_,
246             );
247              
248 1         5 $error->throw;
249 1         10 return $error;
250             }
251              
252             sub serialize {
253 78     78 1 192 my ($self, $content) = @_;
254              
255 78 50       278 my $serializer = $self->serializer
256             or return;
257              
258 78 100       1538 $content = $serializer->serialize($content)
259             or return;
260              
261 77         456 $self->content_type( $serializer->content_type );
262 77         208 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 1.0.0
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