File Coverage

blib/lib/APR/Emulate/PSGI.pm
Criterion Covered Total %
statement 101 139 72.6
branch 26 42 61.9
condition 12 35 34.2
subroutine 24 37 64.8
pod 18 18 100.0
total 181 271 66.7


line stmt bran cond sub pod time code
1             package APR::Emulate::PSGI;
2              
3             =head1 NAME
4              
5             APR::Emulate::PSGI - Class that Emulates the mod_perl2 APR Object (Apache2::RequestRec, et al)
6              
7             =head1 SYNOPSIS
8              
9             use APR::Emulate::PSGI;
10             my $r = APR::Emulate::PSGI->new($psgi_env);
11              
12             # Or in a CGI environment:
13             my $r = APR::Emulate::PSGI->new();
14              
15             =head1 DESCRIPTION
16              
17             This class emulates the mod_perl2 APR object. It expects either a
18             PSGI environment hashref to be passed in, or to read HTTP environment
19             information from the global %ENV.
20              
21             Currently this module is little more than a proof of concept. There
22             are rough edges.
23              
24             Use at your own discretion. Contributions welcome.
25              
26             =cut
27              
28 3     3   75572 use 5.010000;
  3         14  
  3         167  
29 3     3   20 use strict;
  3         7  
  3         139  
30 3     3   29 use warnings;
  3         6  
  3         114  
31              
32 3     3   1116697 use URI;
  3         24970  
  3         103  
33 3     3   3620 use HTTP::Headers;
  3         32001  
  3         6221  
34              
35             # APR::MyPool defined below this package.
36             # APR::MyTable defined below this package.
37              
38             our $VERSION = '0.03';
39              
40             # TODO Replace //= with something 5.6.0 appropriate.
41              
42             =head1 METHODS
43              
44             =over 4
45              
46             =item new
47              
48             Creates an object that emulates the mod_perl2 APR object.
49              
50             my $r = APR::Emulate::PSGI->new($psgi_env);
51              
52             HTTP environment information is read from the PSGI environment that is
53             passed in as a parameter. If no PSGI environment is supplied,
54             environment information is read from the global %ENV.
55              
56             =cut
57              
58             sub new {
59 2     2 1 5131 my ( $class, $env ) = @_;
60 2 100       16 my $self = bless {
61             'psgi_env' => $env,
62             'cgi_mode' => ( defined($env) ? 0 : 1 ),
63             }, $class;
64 2         30 return $self;
65             }
66              
67             =item psgi_status
68              
69             Returns the numeric HTTP response that should be used when building
70             a PSGI response.
71              
72             my $status = $r->psgi_status();
73              
74             The value is determined by looking at the current value of L,
75             or if that is not set, the current value of L, or if that is not
76             set, defaults to 200.
77              
78             =cut
79              
80             sub psgi_status {
81 4     4 1 5 my ($self) = @_;
82 4   50     11 my $status = $self->status_line() || $self->status() || '200';
83 4         8 $status =~ s/\D//g;
84 4         12 return $status;
85             }
86              
87             =item psgi_headers
88              
89             Returns an arrayref of headers which can be used when building a PSGI
90             response.
91              
92             A Content-Length header is not included, and must be added in accordance
93             with the L specification, while building the PSGI response.
94              
95             my $headers_arrayref = $r->psgi_headers();
96              
97             =cut
98              
99             sub psgi_headers {
100 3     3 1 6 my ($self) = @_;
101 3         7 my @headers = ();
102              
103 3         7 my $status = $self->psgi_status();
104 3 50 33     30 if ($status eq '204' || $status eq '304' || $status =~ /^1/) {
      33        
105             # Must not return Content-Type header, per PSGI spec.
106             }
107             else {
108             # Add Content-Type header.
109 3   50     14 push @headers, (
110             'Content-Type',
111             ($self->{'content_type'} || 'text/html'),
112             );
113             }
114              
115             # Add other headers that have been set.
116             $self->headers_out()->do(
117             sub {
118 5     5   7 my ($key, $value) = @_;
119 5         14 push @headers, $key, $value;
120             }
121 3         7 );
122              
123 3         21 return \@headers;
124             };
125              
126             =back
127              
128             =head2 Request Methods
129              
130             =over 4
131              
132             =item headers_in
133              
134             Emulates L.
135              
136             =cut
137              
138             sub headers_in {
139 6     6 1 2841 my ($self) = @_;
140 6 100       40 return $self->{'headers_in'} if (defined($self->{'headers_in'}));
141              
142 2 100       11 my $environment = $self->{'cgi_mode'}
143             ? \%ENV
144             : $self->{'psgi_env'};
145              
146 2         14 my %headers = (
147 34         68 map { $_ => $environment->{$_} }
148 2         14 grep { $_ =~ /^HTTPS?_/ }
149 2         4 keys %{ $environment }
150             );
151              
152 2         9 foreach my $field ('CONTENT_TYPE', 'CONTENT_LENGTH') {
153 4 50       29 $headers{$field} = $environment->{$field} if (defined($environment->{$field}));
154             }
155              
156 2         26 return $self->{'headers_in'} = HTTP::Headers->new(%headers);
157             }
158              
159             =item method
160              
161             Emulates L.
162              
163             =cut
164              
165             sub method {
166 2     2 1 2875 my ($self) = @_;
167 2 100       19 if ($self->{'cgi_mode'}) {
168 1         7 return $ENV{'REQUEST_METHOD'};
169             }
170 1         8 return $self->{'psgi_env'}{'REQUEST_METHOD'};
171             }
172              
173             =item uri
174              
175             Emulates L.
176              
177             =cut
178              
179             sub uri {
180 0     0 1 0 my ($self) = @_;
181 0 0       0 if ($self->{'cgi_mode'}) {
182 0         0 return $ENV{'PATH_INFO'};
183             }
184 0         0 return $self->{'psgi_env'}{'PATH_INFO'};
185             }
186              
187             =item parsed_uri
188              
189             Emulates L.
190              
191             =cut
192              
193             sub parsed_uri {
194 0     0 1 0 my ($self) = @_;
195 0 0       0 if ($self->{'cgi_mode'}) {
196 0   0     0 return $self->{'uri'} //= URI->new($ENV{'REQUEST_URI'});
197             }
198 0   0     0 return $self->{'uri'} //= URI->new($self->{'psgi_env'}{'REQUEST_URI'});
199             }
200              
201             =item args
202              
203             Emulates L.
204              
205             =cut
206              
207             sub args {
208 0     0 1 0 my ($self) = @_;
209 0 0       0 if ($self->{'cgi_mode'}) {
210 0         0 return $ENV{'QUERY_STRING'};
211             }
212 0         0 return $self->{'psgi_env'}{'QUERY_STRING'};
213             }
214              
215             =item read
216              
217             Emulates L.
218              
219             =cut
220              
221             sub read {
222 2     2 1 1547 my ($self, $buffer, $length, $offset) = @_;
223 2   50     18 $offset ||= 0;
224             # We use $_[1] instead of $buffer, because we need to modify the original instead of a copy.
225 2 100       12 if ($self->{'cgi_mode'}) {
226 1         18 return CORE::read(\*STDIN, $_[1], $length, $offset);
227             }
228 1         15 return $self->{'psgi_env'}{'psgi.input'}->read($_[1], $length, $offset);
229             }
230              
231             =item pool
232              
233             Emulates L.
234              
235             =cut
236              
237             sub pool {
238 0     0 1 0 my ($self) = @_;
239 0   0     0 return $self->{'pool'} //= APR::MyPool->new();
240             }
241              
242             =back
243              
244             =head2 Response Methods
245              
246             =over 4
247              
248             =item headers_out
249              
250             Emulates L.
251              
252             =cut
253              
254             sub headers_out {
255 9     9 1 2125 my ($self) = @_;
256 9   66     70 return $self->{'headers_out'} //= APR::MyTable::make();
257             }
258              
259             =item err_headers_out
260              
261             Emulates L.
262              
263             =cut
264              
265             sub err_headers_out {
266 0     0 1 0 my ($self) = @_;
267 0   0     0 return $self->{'err_headers_out'} //= APR::MyTable::make();
268             }
269              
270             =item no_cache
271              
272             Emulates L.
273              
274             =cut
275              
276             sub no_cache {
277 2     2 1 2097 my ($self, $value) = @_;
278 2   100     11 my $previous_value = $self->{'no_cache'} || 0;
279 2 100       9 $self->{'no_cache'} = $value ? 1 : 0;
280 2 50       7 return $previous_value if ($previous_value eq $self->{'no_cache'});
281              
282             # Set headers.
283 2 100       5 if ($self->{'no_cache'}) {
284 1         4 $self->headers_out()->add('Pragma' => 'no-cache');
285 1         2 $self->headers_out()->add('Cache-control' => 'no-cache');
286             }
287             # Unset headers.
288             else {
289 1         4 $self->headers_out()->unset('Pragma', 'Cache-control');
290             }
291 2         10 return $previous_value;
292             }
293              
294             =item status
295              
296             Emulates L.
297              
298             =cut
299              
300             sub status {
301 5     5 1 8 my ($self, @value) = @_;
302 5 50       14 $self->{'status'} = $value[0] if scalar(@value);
303 5         56 return $self->{'status'};
304             }
305              
306             =item status_line
307              
308             Emulates L.
309              
310             =cut
311              
312             sub status_line {
313 5     5 1 8 my ($self, @value) = @_;
314 5 50       38 $self->{'status_line'} = $value[0] if scalar(@value);
315 5         34 return $self->{'status_line'};
316             }
317              
318             =item content_type
319              
320             Emulates L.
321              
322             If no PSGI enviroment is provided to L, calling this
323             method with a parameter will cause HTTP headers to be sent.
324              
325             =cut
326              
327             sub content_type {
328 2     2 1 7 my ($self, @value) = @_;
329 2 50       9 if (scalar(@value)) {
330 2         9 $self->{'content_type'} = $value[0];
331              
332 2 100       9 if ($self->{'cgi_mode'}) {
333 1         6 $self->_send_http_headers();
334             }
335             }
336 2         19 return $self->{'content_type'};
337             }
338              
339             sub _send_http_headers {
340 1     1   3 my ($self) = @_;
341 1 50       5 return if ($self->{'headers_sent'});
342 1 50 50     5 if (my $status = $self->status_line() || $self->status() || '200 OK') {
343 1   50     9 my $url_scheme = uc($self->{'psgi_env'}{'psgi.url_scheme'} || 'http');
344 1         14 print $url_scheme . '/1.1 ' . $status . "\n";
345             }
346 1   50     6 print 'Content-Type: ' . ($self->{'content_type'} || 'text/html') . "\n";
347             $self->headers_out()->do(
348             sub {
349 1     1   2 my ($key, $value) = @_;
350 1         6 print join(': ', $key, $value) . "\n";
351             }
352 1         4 );
353 1         5 print "\n\n";
354 1         3 $self->{'headers_sent'} = 1;
355 1         2 return 1;
356             }
357              
358             =item print
359              
360             Emulates L.
361              
362             =cut
363              
364             sub print {
365 2     2 1 5902 my ($self, @content) = @_;
366 2         31 my $success = CORE::print @content;
367 2 50       23 return $success
368             ? length(join('', @content))
369             : 0;
370             }
371              
372             =item rflush
373              
374             Emulates L.
375              
376             =cut
377              
378 0     0 1 0 sub rflush {}
379              
380             =back
381              
382             =cut
383              
384             # See APR::Table in mod_perl 2 distribution.
385             package APR::MyTable;
386              
387             sub make {
388 2     2   38 return bless {}, __PACKAGE__;
389             }
390              
391             sub copy {
392 0     0   0 my ($self) = @_;
393 0         0 my %copy = %$self;
394 0         0 return bless \%copy, ref($self);
395             }
396              
397             sub clear {
398 0     0   0 my ($self) = @_;
399 0         0 my (@keys) = keys %$self;
400 0         0 foreach my $key (@keys) {
401 0         0 delete $self->{$key};
402             }
403 0         0 return 1;
404             }
405              
406             sub set {
407 0     0   0 my ($self, @pairs) = @_;
408 0         0 while (@pairs) {
409 0         0 my ($key, $value) = splice(@pairs, 0, 2);
410 0         0 $self->{$key} = $value;
411             }
412 0         0 return 1;
413             }
414              
415             sub unset {
416 1     1   3 my ($self, @keys) = @_;
417 1         2 foreach my $key (@keys) {
418 2         5 delete $self->{$key};
419             }
420 1         2 return 1;
421             }
422              
423             sub add {
424             # TODO: When implemented properly, this should allow duplicate keys.
425 4     4   10 my ($self, $key, $value) = @_;
426 4         14 $self->{$key} = $value;
427 4         13 return 1;
428             }
429              
430             sub get {
431             # TODO: When implemented properly, this should allow duplicate keys.
432 0     0   0 my ($self, $key) = @_;
433 0         0 return $self->{$key};
434             }
435              
436             sub merge {
437             # TODO: Not yet implemented.
438 0     0   0 return undef;
439             }
440              
441             sub do {
442 4     4   10 my ($self, $code, @keys) = @_;
443 4 50       21 @keys = keys %$self if (scalar(@keys) == 0);
444 4         8 foreach my $key (@keys) {
445 6         15 $code->($key, $self->{$key});
446             }
447 4         8 return 1;
448             }
449              
450             package APR::MyPool;
451              
452             sub new {
453 0     0     bless {}, $_[0];
454             }
455              
456             sub cleanup_register {
457 0     0     my ($self, $code, @args) = @_;
458 0           foreach my $arg (@args) {
459 0           $code->($arg);
460             }
461 0           return 1;
462             }
463              
464             1;
465             __END__