File Coverage

blib/lib/PAGI/App/WrapPSGI.pm
Criterion Covered Total %
statement 9 117 7.6
branch 0 38 0.0
condition 0 8 0.0
subroutine 3 15 20.0
pod 0 2 0.0
total 12 180 6.6


line stmt bran cond sub pod time code
1             package PAGI::App::WrapPSGI;
2             $PAGI::App::WrapPSGI::VERSION = '0.002000';
3 1     1   771 use strict;
  1         2  
  1         34  
4 1     1   3 use warnings;
  1         1  
  1         35  
5 1     1   3 use Future::AsyncAwait;
  1         14  
  1         4  
6              
7              
8             =head1 NAME
9              
10             PAGI::App::WrapPSGI - PSGI-to-PAGI adapter
11              
12             =head1 SYNOPSIS
13              
14             use PAGI::App::WrapPSGI;
15              
16             my $psgi_app = sub {
17             my ($env) = @_;
18             return [200, ['Content-Type' => 'text/plain'], ['Hello']];
19             };
20              
21             my $wrapper = PAGI::App::WrapPSGI->new(psgi_app => $psgi_app);
22             my $pagi_app = $wrapper->to_app;
23              
24             =head1 DESCRIPTION
25              
26             PAGI::App::WrapPSGI wraps a PSGI application to make it work with
27             PAGI servers. It converts PAGI scope to PSGI %env and converts
28             PSGI responses to PAGI events.
29              
30             =cut
31              
32             sub new {
33 0     0 0   my ($class, %args) = @_;
34              
35             my $self = bless {
36             psgi_app => $args{psgi_app},
37 0           }, $class;
38 0           return $self;
39             }
40              
41             sub to_app {
42 0     0 0   my ($self) = @_;
43              
44 0           my $psgi_app = $self->{psgi_app};
45              
46 0     0     return async sub {
47 0           my ($scope, $receive, $send) = @_;
48 0 0         die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'http';
49              
50 0           my $env = $self->_build_env($scope);
51              
52             # Collect request body
53 0           my $body = '';
54 0           while (1) {
55 0           my $event = await $receive->();
56 0 0         last if $event->{type} ne 'http.request';
57 0   0       $body .= $event->{body} // '';
58 0 0         last unless $event->{more};
59             }
60              
61             # Create psgi.input
62 0 0         open my $input, '<', \$body or die $!;
63 0           $env->{'psgi.input'} = $input;
64              
65             # Call PSGI app
66 0           my $response = $psgi_app->($env);
67              
68             # Handle response - could be arrayref or coderef (streaming)
69 0 0         if (ref $response eq 'CODE') {
70             # Delayed/streaming response
71 0           await $self->_handle_streaming_response($send, $response);
72             } else {
73 0           await $self->_send_response($send, $response);
74             }
75 0           };
76             }
77              
78             sub _build_env {
79 0     0     my ($self, $scope) = @_;
80              
81             my %env = (
82             REQUEST_METHOD => $scope->{method},
83             SCRIPT_NAME => $scope->{root_path},
84             PATH_INFO => $scope->{path},
85             QUERY_STRING => $scope->{query_string},
86             SERVER_PROTOCOL => 'HTTP/' . $scope->{http_version},
87             'psgi.version' => [1, 1],
88             'psgi.url_scheme' => $scope->{scheme},
89 0           'psgi.errors' => \*STDERR,
90             'psgi.multithread' => 0,
91             'psgi.multiprocess' => 0,
92             'psgi.run_once' => 0,
93             'psgi.streaming' => 1,
94             'psgi.nonblocking' => 1,
95             );
96              
97             # Add headers
98 0           for my $header (@{$scope->{headers}}) {
  0            
99 0           my ($name, $value) = @$header;
100 0           my $key = uc($name);
101 0           $key =~ s/-/_/g;
102 0 0         if ($key eq 'CONTENT_TYPE') {
    0          
103 0           $env{CONTENT_TYPE} = $value;
104             } elsif ($key eq 'CONTENT_LENGTH') {
105 0           $env{CONTENT_LENGTH} = $value;
106             } else {
107 0           $env{"HTTP_$key"} = $value;
108             }
109             }
110              
111             # Server/client info
112 0 0         if ($scope->{server}) {
113 0           $env{SERVER_NAME} = $scope->{server}[0];
114 0           $env{SERVER_PORT} = $scope->{server}[1];
115             }
116 0 0         if ($scope->{client}) {
117 0           $env{REMOTE_ADDR} = $scope->{client}[0];
118 0           $env{REMOTE_PORT} = $scope->{client}[1];
119             }
120              
121 0           return \%env;
122             }
123              
124 0     0     async sub _send_response {
125 0           my ($self, $send, $response) = @_;
126              
127 0           my ($status, $headers, $body) = @$response;
128              
129             await $send->({
130             type => 'http.response.start',
131             status => $status,
132 0           headers => [ map { [lc($_->[0]), $_->[1]] } @{_pairs($headers)} ],
133             });
134              
135 0 0         if (ref $body eq 'ARRAY') {
    0          
136 0           my $content = join '', @$body;
137 0           await $send->({
138             type => 'http.response.body',
139             body => $content,
140             more => 0,
141             });
142             } elsif (ref $body eq 'CODE') {
143             # Streaming response body (coderef)
144             # This is the "pull" pattern where we call the coderef repeatedly
145 0           while (1) {
146 0           my $chunk = $body->();
147 0 0         last unless defined $chunk;
148 0           await $send->({
149             type => 'http.response.body',
150             body => $chunk,
151             more => 1,
152             });
153             }
154             await $send->({
155             type => 'http.response.body',
156             body => '',
157             more => 0,
158 0           });
159             } else {
160             # Filehandle
161 0           local $/;
162 0           my $content = <$body>;
163 0           await $send->({
164             type => 'http.response.body',
165             body => $content // '',
166             more => 0,
167             });
168             }
169             }
170              
171             # Handle PSGI delayed/streaming response pattern
172 0     0     async sub _handle_streaming_response {
173 0           my ($self, $send, $responder_callback) = @_;
174              
175 0           my @body_chunks;
176 0           my $response_started = 0;
177 0           my $writer;
178              
179             # Create a writer object for streaming
180             my $create_writer = sub {
181 0     0     my ($send_ref, $status, $headers) = @_;
182             return {
183             write => sub {
184 0           my ($chunk) = @_;
185 0           push @body_chunks, $chunk;
186             },
187             close => sub {
188             # Mark as closed - will be handled after responder returns
189             },
190 0           };
191 0           };
192              
193             # Create the responder callback for the PSGI app
194             my $responder = sub {
195 0     0     my ($response) = @_;
196              
197 0 0         if (@$response == 3) {
    0          
198             # Complete response [status, headers, body]
199 0           my ($status, $headers, $body) = @$response;
200 0           $response_started = 1;
201              
202             # Store for later sending
203 0           push @body_chunks, { status => $status, headers => $headers, body => $body };
204 0           return;
205             } elsif (@$response == 2) {
206             # Streaming response [status, headers] - return writer
207 0           my ($status, $headers) = @$response;
208 0           $response_started = 1;
209              
210             # Store header info
211 0           push @body_chunks, { status => $status, headers => $headers };
212              
213             # Return a writer object
214 0           $writer = bless {
215             chunks => \@body_chunks,
216             }, 'PAGI::App::WrapPSGI::Writer';
217              
218 0           return $writer;
219             }
220 0           };
221              
222             # Call the PSGI delayed response callback
223 0           $responder_callback->($responder);
224              
225             # Now send all the collected response data
226 0 0         if (@body_chunks) {
227 0           my $first = shift @body_chunks;
228              
229 0 0         if (ref $first eq 'HASH') {
230 0           my $status = $first->{status};
231 0           my $headers = $first->{headers};
232 0           my $body = $first->{body};
233              
234             await $send->({
235             type => 'http.response.start',
236             status => $status,
237 0           headers => [ map { [lc($_->[0]), $_->[1]] } @{_pairs($headers)} ],
238             });
239              
240 0 0         if (defined $body) {
241             # Complete response with body
242 0           await $self->_send_body($send, $body);
243             } else {
244             # Streaming - send collected chunks
245 0           for my $chunk (@body_chunks) {
246 0           await $send->({
247             type => 'http.response.body',
248             body => $chunk,
249             more => 1,
250             });
251             }
252             await $send->({
253             type => 'http.response.body',
254             body => '',
255             more => 0,
256 0           });
257             }
258             }
259             }
260             }
261              
262 0     0     async sub _send_body {
263 0           my ($self, $send, $body) = @_;
264              
265 0 0 0       if (ref $body eq 'ARRAY') {
    0 0        
266 0           my $content = join '', @$body;
267 0           await $send->({
268             type => 'http.response.body',
269             body => $content,
270             more => 0,
271             });
272             } elsif (ref $body eq 'GLOB' || (ref $body && $body->can('getline'))) {
273             # Filehandle
274 0           local $/;
275 0           my $content = <$body>;
276 0           await $send->({
277             type => 'http.response.body',
278             body => $content // '',
279             more => 0,
280             });
281             } else {
282 0           await $send->({
283             type => 'http.response.body',
284             body => $body // '',
285             more => 0,
286             });
287             }
288             }
289              
290             # Simple writer class for streaming responses
291             package PAGI::App::WrapPSGI::Writer;
292              
293             sub write {
294 0     0     my ($self, $chunk) = @_;
295 0           push @{$self->{chunks}}, $chunk;
  0            
296             }
297              
298             sub close {
299 0     0     my ($self) = @_;
300             # Nothing special needed - chunks are already collected
301             }
302              
303             package PAGI::App::WrapPSGI;
304              
305             sub _pairs {
306 0     0     my ($arrayref) = @_;
307              
308 0           my @pairs;
309 0           for (my $i = 0; $i < @$arrayref; $i += 2) {
310 0           push @pairs, [$arrayref->[$i], $arrayref->[$i+1]];
311             }
312 0           return \@pairs;
313             }
314              
315             1;
316              
317             __END__