File Coverage

blib/lib/PAGI/Endpoint/HTTP.pm
Criterion Covered Total %
statement 50 50 100.0
branch 13 16 81.2
condition 11 19 57.8
subroutine 11 11 100.0
pod 4 5 80.0
total 89 101 88.1


line stmt bran cond sub pod time code
1             package PAGI::Endpoint::HTTP;
2             $PAGI::Endpoint::HTTP::VERSION = '0.002000';
3 7     7   999489 use strict;
  7         10  
  7         226  
4 7     7   27 use warnings;
  7         18  
  7         272  
5              
6 7     7   27 use Future::AsyncAwait;
  7         22  
  7         33  
7 7     7   299 use Carp qw(croak);
  7         10  
  7         300  
8 7     7   25 use Scalar::Util qw(blessed);
  7         9  
  7         5639  
9              
10             # Factory class method - override in subclass for customization
11 10     10 1 4120 sub context_class { 'PAGI::Context' }
12              
13             sub new {
14 16     16 0 366886 my ($class, %args) = @_;
15 16         34 return bless \%args, $class;
16             }
17              
18             # HTTP methods we support
19             our @HTTP_METHODS = qw(get post put patch delete head options);
20              
21             sub allowed_methods {
22 6     6 1 13 my ($self) = @_;
23 6         7 my @allowed;
24 6         13 for my $method (@HTTP_METHODS) {
25 42 100       146 push @allowed, uc($method) if $self->can($method);
26             }
27             # HEAD is allowed if GET is defined
28 6 50 33     42 push @allowed, 'HEAD' if $self->can('get') && !$self->can('head');
29             # OPTIONS is always allowed
30 6 50       15 push @allowed, 'OPTIONS' unless grep { $_ eq 'OPTIONS' } @allowed;
  19         35  
31 6         45 return sort @allowed;
32             }
33              
34 13     13 1 34 async sub dispatch {
35 13         31 my ($self, $ctx) = @_;
36 13   50     40 my $http_method = lc($ctx->method // 'GET');
37              
38 13         28 my $res;
39              
40             # OPTIONS - return allowed methods (auto-respond unless overridden)
41 13 100 66     160 if ($http_method eq 'options' && !$self->can('options')) {
    100 66        
    100 66        
42 2         8 my $allow = join(', ', $self->allowed_methods);
43 2         8 $res = $ctx->response->header('Allow', $allow)->empty;
44             }
45             # HEAD falls back to GET if not explicitly defined
46             elsif ($http_method eq 'head' && !$self->can('head') && $self->can('get')) {
47 1         3 $res = await $self->get($ctx);
48             }
49             # Dispatch to the appropriate method handler
50             elsif ($self->can($http_method)) {
51 7         24 $res = await $self->$http_method($ctx);
52             }
53             # 405 Method Not Allowed
54             else {
55 3         13 my $allow = join(', ', $self->allowed_methods);
56 3         13 $res = $ctx->response->header('Allow', $allow)->status(405)->text("405 Method Not Allowed");
57             }
58              
59 13 100 66     576 croak ref($self) . "->$http_method did not return a response"
60             unless blessed($res) && $res->can('respond');
61 12         40 await $ctx->respond($res);
62             }
63              
64             sub to_app {
65 8     8 1 387373 my ($class) = @_;
66 8         32 my $context_class = $class->context_class;
67 8         29 my $endpoint = $class->new; # ONE instance for the app lifetime (singleton)
68              
69 7     7   175 return async sub {
70 7         8 my ($scope, $receive, $send) = @_;
71              
72 7   50     21 my $type = $scope->{type} // 'http';
73 7 50       14 croak "Expected http scope, got '$type'" unless $type eq 'http';
74              
75 7         1235 require PAGI::Context;
76 7         41 my $ctx = $context_class->new($scope, $receive, $send);
77              
78 7         31 await $endpoint->dispatch($ctx);
79 8         42 };
80             }
81              
82             1;
83              
84             __END__