PAGI

 view release on metacpan or  search on metacpan

lib/PAGI/App/WrapPSGI.pm  view on Meta::CPAN

=head1 NAME

PAGI::App::WrapPSGI - PSGI-to-PAGI adapter

=head1 SYNOPSIS

    use PAGI::App::WrapPSGI;

    my $psgi_app = sub {
        my ($env) = @_;
        return [200, ['Content-Type' => 'text/plain'], ['Hello']];
    };

    my $wrapper = PAGI::App::WrapPSGI->new(psgi_app => $psgi_app);
    my $pagi_app = $wrapper->to_app;

=head1 DESCRIPTION

PAGI::App::WrapPSGI wraps a PSGI application to make it work with
PAGI servers. It converts PAGI scope to PSGI %env and converts
PSGI responses to PAGI events.

=cut

sub new {
    my ($class, %args) = @_;

    my $self = bless {
        psgi_app => $args{psgi_app},
    }, $class;
    return $self;
}

sub to_app {
    my ($self) = @_;

    my $psgi_app = $self->{psgi_app};

    return async sub  {
        my ($scope, $receive, $send) = @_;
        die "Unsupported scope type: $scope->{type}" if $scope->{type} ne 'http';

        my $env = $self->_build_env($scope);

        # Collect request body
        my $body = '';
        while (1) {
            my $event = await $receive->();
            last if $event->{type} ne 'http.request';
            $body .= $event->{body} // '';
            last unless $event->{more};
        }

        # Create psgi.input
        open my $input, '<', \$body or die $!;
        $env->{'psgi.input'} = $input;

        # Call PSGI app
        my $response = $psgi_app->($env);

        # Handle response - could be arrayref or coderef (streaming)
        if (ref $response eq 'CODE') {
            # Delayed/streaming response
            await $self->_handle_streaming_response($send, $response);
        } else {
            await $self->_send_response($send, $response);
        }
    };
}

sub _build_env {
    my ($self, $scope) = @_;

    my %env = (
        REQUEST_METHOD  => $scope->{method},
        SCRIPT_NAME     => $scope->{root_path},
        PATH_INFO       => $scope->{path},
        QUERY_STRING    => $scope->{query_string},
        SERVER_PROTOCOL => 'HTTP/' . $scope->{http_version},
        'psgi.version'    => [1, 1],
        'psgi.url_scheme' => $scope->{scheme},
        'psgi.errors'     => \*STDERR,
        'psgi.multithread'  => 0,
        'psgi.multiprocess' => 0,
        'psgi.run_once'     => 0,
        'psgi.streaming'    => 1,
        'psgi.nonblocking'  => 1,
    );

    # Add headers
    for my $header (@{$scope->{headers}}) {
        my ($name, $value) = @$header;
        my $key = uc($name);
        $key =~ s/-/_/g;
        if ($key eq 'CONTENT_TYPE') {
            $env{CONTENT_TYPE} = $value;
        } elsif ($key eq 'CONTENT_LENGTH') {
            $env{CONTENT_LENGTH} = $value;
        } else {
            $env{"HTTP_$key"} = $value;
        }
    }

    # Server/client info
    if ($scope->{server}) {
        $env{SERVER_NAME} = $scope->{server}[0];
        $env{SERVER_PORT} = $scope->{server}[1];
    }
    if ($scope->{client}) {
        $env{REMOTE_ADDR} = $scope->{client}[0];
        $env{REMOTE_PORT} = $scope->{client}[1];
    }

    return \%env;
}

async sub _send_response {
    my ($self, $send, $response) = @_;

    my ($status, $headers, $body) = @$response;

    await $send->({
        type    => 'http.response.start',
        status  => $status,
        headers => [ map { [lc($_->[0]), $_->[1]] } @{_pairs($headers)} ],
    });

    if (ref $body eq 'ARRAY') {
        my $content = join '', @$body;
        await $send->({
            type => 'http.response.body',
            body => $content,
            more => 0,
        });
    } elsif (ref $body eq 'CODE') {
        # Streaming response body (coderef)
        # This is the "pull" pattern where we call the coderef repeatedly
        while (1) {
            my $chunk = $body->();
            last unless defined $chunk;
            await $send->({
                type => 'http.response.body',
                body => $chunk,
                more => 1,
            });
        }
        await $send->({
            type => 'http.response.body',
            body => '',
            more => 0,
        });
    } else {
        # Filehandle
        local $/;
        my $content = <$body>;
        await $send->({
            type => 'http.response.body',
            body => $content // '',
            more => 0,
        });
    }
}

# Handle PSGI delayed/streaming response pattern
async sub _handle_streaming_response {
    my ($self, $send, $responder_callback) = @_;

    my @body_chunks;
    my $response_started = 0;
    my $writer;

    # Create a writer object for streaming
    my $create_writer = sub  {
        my ($send_ref, $status, $headers) = @_;
        return {
            write => sub {
                my ($chunk) = @_;
                push @body_chunks, $chunk;
            },
            close => sub {
                # Mark as closed - will be handled after responder returns
            },
        };
    };

    # Create the responder callback for the PSGI app
    my $responder = sub {
        my ($response) = @_;

        if (@$response == 3) {
            # Complete response [status, headers, body]
            my ($status, $headers, $body) = @$response;
            $response_started = 1;

            # Store for later sending
            push @body_chunks, { status => $status, headers => $headers, body => $body };
            return;
        } elsif (@$response == 2) {
            # Streaming response [status, headers] - return writer
            my ($status, $headers) = @$response;
            $response_started = 1;

            # Store header info
            push @body_chunks, { status => $status, headers => $headers };

            # Return a writer object
            $writer = bless {
                chunks => \@body_chunks,
            }, 'PAGI::App::WrapPSGI::Writer';

            return $writer;
        }
    };

    # Call the PSGI delayed response callback
    $responder_callback->($responder);

    # Now send all the collected response data
    if (@body_chunks) {
        my $first = shift @body_chunks;

        if (ref $first eq 'HASH') {
            my $status = $first->{status};
            my $headers = $first->{headers};
            my $body = $first->{body};

            await $send->({
                type    => 'http.response.start',
                status  => $status,
                headers => [ map { [lc($_->[0]), $_->[1]] } @{_pairs($headers)} ],
            });

            if (defined $body) {
                # Complete response with body
                await $self->_send_body($send, $body);
            } else {
                # Streaming - send collected chunks
                for my $chunk (@body_chunks) {
                    await $send->({
                        type => 'http.response.body',
                        body => $chunk,
                        more => 1,
                    });
                }
                await $send->({
                    type => 'http.response.body',
                    body => '',
                    more => 0,
                });
            }
        }
    }
}

async sub _send_body {
    my ($self, $send, $body) = @_;

    if (ref $body eq 'ARRAY') {
        my $content = join '', @$body;
        await $send->({
            type => 'http.response.body',
            body => $content,
            more => 0,
        });
    } elsif (ref $body eq 'GLOB' || (ref $body && $body->can('getline'))) {
        # Filehandle
        local $/;
        my $content = <$body>;
        await $send->({
            type => 'http.response.body',
            body => $content // '',
            more => 0,
        });
    } else {
        await $send->({
            type => 'http.response.body',
            body => $body // '',
            more => 0,
        });
    }
}

# Simple writer class for streaming responses
package PAGI::App::WrapPSGI::Writer;

sub write {
    my ($self, $chunk) = @_;
    push @{$self->{chunks}}, $chunk;
}

sub close {
    my ($self) = @_;
    # Nothing special needed - chunks are already collected
}

package PAGI::App::WrapPSGI;

sub _pairs {
    my ($arrayref) = @_;

    my @pairs;
    for (my $i = 0; $i < @$arrayref; $i += 2) {
        push @pairs, [$arrayref->[$i], $arrayref->[$i+1]];
    }
    return \@pairs;
}

1;

__END__

=head1 SEE ALSO

L<PAGI::Server>, L<PSGI>

=head1 AUTHOR

John Napiorkowski E<lt>jjnapiork@cpan.orgE<gt>

=head1 LICENSE

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut



( run in 0.755 second using v1.01-cache-2.11-cpan-140bd7fdf52 )