File Coverage

blib/lib/PAGI/Middleware.pm
Criterion Covered Total %
statement 34 39 87.1
branch 3 6 50.0
condition 2 4 50.0
subroutine 10 11 90.9
pod 6 6 100.0
total 55 66 83.3


line stmt bran cond sub pod time code
1             package PAGI::Middleware;
2              
3 26     26   207570 use strict;
  26         50  
  26         815  
4 26     26   102 use warnings;
  26         67  
  26         1052  
5 26     26   1390 use Future::AsyncAwait;
  26         52312  
  26         263  
6              
7             =head1 NAME
8              
9             PAGI::Middleware - Base class for PAGI middleware
10              
11             =head1 SYNOPSIS
12              
13             # Create a custom middleware
14             package My::Middleware;
15             use parent 'PAGI::Middleware';
16              
17             sub wrap {
18             my ($self, $app) = @_;
19              
20             return async sub {
21             my ($scope, $receive, $send) = @_;
22             # Modify scope for inner app
23             my $modified_scope = $self->modify_scope($scope, {
24             custom_key => 'custom_value',
25             });
26              
27             # Call inner app with modified scope
28             await $app->($modified_scope, $receive, $send);
29             };
30             }
31              
32             # Use the builder DSL
33             use PAGI::Middleware::Builder;
34              
35             my $app = builder {
36             enable 'My::Middleware', option => 'value';
37             enable_if { $_[0]->{path} =~ m{^/api/} } 'RateLimit', limit => 100;
38             mount '/static' => $static_app;
39             $my_app;
40             };
41              
42             =head1 DESCRIPTION
43              
44             PAGI::Middleware provides a base class for implementing middleware that wraps
45             PAGI applications. Middleware can modify the request scope, intercept responses,
46             or perform cross-cutting concerns like logging, authentication, or compression.
47              
48             =head1 METHODS
49              
50             =head2 new
51              
52             my $middleware = PAGI::Middleware->new(%config);
53              
54             Create a new middleware instance. Configuration options are stored and
55             accessible via C<$self-E{config}>.
56              
57             =cut
58              
59             sub new {
60 210     210 1 4130963 my ($class, %config) = @_;
61              
62 210         539 my $self = bless {
63             config => \%config,
64             }, $class;
65 210         854 $self->_init(\%config);
66 208         427 return $self;
67             }
68              
69             =head2 _init
70              
71             $self->_init(\%config);
72              
73             Hook for subclasses to perform initialization. Called by new().
74             Default implementation does nothing.
75              
76             =cut
77              
78             sub _init {
79 20     20   31 my ($self, $config) = @_;
80              
81             # Subclasses can override
82             }
83              
84             =head2 wrap
85              
86             my $wrapped_app = $middleware->wrap($app);
87              
88             Wrap a PAGI application. Returns a new async sub that handles
89             the middleware logic. Subclasses MUST override this method.
90              
91             =cut
92              
93             sub wrap {
94 1     1 1 22 my ($self, $app) = @_;
95              
96 1         7 die "Subclass must implement wrap()";
97             }
98              
99             =head2 modify_scope
100              
101             my $new_scope = $self->modify_scope($scope, \%additions);
102              
103             Create a new scope with additional keys, without mutating the original.
104             This is the recommended way to pass additional data to inner apps.
105              
106             =cut
107              
108             sub modify_scope {
109 15     15 1 73 my ($self, $scope, $additions) = @_;
110 15   50     33 $additions //= {};
111              
112 15         69 return { %$scope, %$additions };
113             }
114              
115             =head2 intercept_send
116              
117             my $wrapped_send = $self->intercept_send($send, \&interceptor);
118              
119             Wrap the $send callback to intercept outgoing events.
120             The interceptor is called with ($event, $original_send) and should
121             return a Future.
122              
123             my $wrapped_send = $self->intercept_send($send, async sub {
124             my ($event, $original_send) = @_;
125             if ($event->{type} eq 'http.response.start') {
126             # Modify headers
127             push @{$event->{headers}}, ['x-custom', 'value'];
128             }
129             await $original_send->($event);
130             });
131              
132             =cut
133              
134             sub intercept_send {
135 2     2 1 236 my ($self, $send, $interceptor) = @_;
136              
137 3     3   122 return async sub {
138 3         3 my ($event) = @_;
139 3         21 await $interceptor->($event, $send);
140 2         7 };
141             }
142              
143             =head2 buffer_request_body
144              
145             my ($body, $final_event) = await $self->buffer_request_body($receive);
146              
147             Collect all request body chunks into a single string.
148             Returns the complete body and the final http.request event.
149              
150             =cut
151              
152 1     1 1 19 async sub buffer_request_body {
153 1         2 my ($self, $receive) = @_;
154              
155 1         2 my $body = '';
156 1         1 my $event;
157              
158 1         1 while (1) {
159 2         4 $event = await $receive->();
160              
161 2 50       62 if ($event->{type} eq 'http.request') {
    0          
162 2   50     6 $body .= $event->{body} // '';
163 2 100       6 last unless $event->{more};
164             } elsif ($event->{type} eq 'http.disconnect') {
165 0         0 last;
166             }
167             }
168              
169 1         3 return ($body, $event);
170             }
171              
172             =head2 call
173              
174             await $middleware->call($scope, $receive, $send, $app);
175              
176             Convenience method to invoke the middleware with an app.
177             Equivalent to:
178              
179             my $wrapped = $middleware->wrap($app);
180             await $wrapped->($scope, $receive, $send);
181              
182             =cut
183              
184 0     0 1   async sub call {
185 0           my ($self, $scope, $receive, $send, $app) = @_;
186              
187 0           my $wrapped = $self->wrap($app);
188 0           await $wrapped->($scope, $receive, $send);
189             }
190              
191             1;
192              
193             __END__