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             $PAGI::Middleware::VERSION = '0.002000';
3 26     26   221636 use strict;
  26         65  
  26         868  
4 26     26   98 use warnings;
  26         51  
  26         1089  
5 26     26   1900 use Future::AsyncAwait;
  26         71378  
  26         245  
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 208     208 1 4550548 my ($class, %config) = @_;
61              
62 208         664 my $self = bless {
63             config => \%config,
64             }, $class;
65 208         992 $self->_init(\%config);
66 206         521 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 25     25   40 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 52 my ($self, $app) = @_;
95              
96 1         26 die "Subclass must implement wrap()";
97             }
98              
99             =head2 modify_scope
100              
101             my $new_scope = $self->modify_scope($scope, \%additions);
102              
103             Return a new scope -- a shallow copy of C<$scope> with C<%additions> merged in --
104             B. This is the supported way to pass extra data to
105             inner apps, and the canonical middleware in this distribution use it.
106              
107             Why copy instead of writing C<< $scope->{key} = ... >> directly? Middleware form a
108             stack, and the scope you are handed is shared with the layers around you. Mutating
109             it in place lets your change leak B to parent and sibling layers (and, for
110             a long-lived WebSocket scope, persist across every event on the connection).
111             Copying keeps your additions B -- seen by the inner app you call,
112             invisible to everyone above.
113              
114             The copy is B on purpose. Top-level keys you add are private to the new
115             scope, but values that are B -- the C object, the
116             lifespan C namespace, the stash -- are shared, so the inner app still sees
117             the same connection state and shared objects. A deep copy would sever those. One
118             corollary worth knowing: a plain B set as a top-level scope key does not
119             propagate back to outer layers, so to share mutable state across layers you mutate
120             B (see L). The full model -- and why it works
121             this way -- is in L.
122              
123             =cut
124              
125             sub modify_scope {
126 50     50 1 187 my ($self, $scope, $additions) = @_;
127 50   50     127 $additions //= {};
128              
129 50         352 return { %$scope, %$additions };
130             }
131              
132             =head2 intercept_send
133              
134             my $wrapped_send = $self->intercept_send($send, \&interceptor);
135              
136             Wrap the $send callback to intercept outgoing events.
137             The interceptor is called with ($event, $original_send) and should
138             return a Future.
139              
140             my $wrapped_send = $self->intercept_send($send, async sub {
141             my ($event, $original_send) = @_;
142             if ($event->{type} eq 'http.response.start') {
143             # Modify headers
144             push @{$event->{headers}}, ['x-custom', 'value'];
145             }
146             await $original_send->($event);
147             });
148              
149             =cut
150              
151             sub intercept_send {
152 2     2 1 313 my ($self, $send, $interceptor) = @_;
153              
154 3     3   111 return async sub {
155 3         5 my ($event) = @_;
156 3         17 await $interceptor->($event, $send);
157 2         10 };
158             }
159              
160             =head2 buffer_request_body
161              
162             my ($body, $final_event) = await $self->buffer_request_body($receive);
163              
164             Collect all request body chunks into a single string.
165             Returns the complete body and the final http.request event.
166              
167             =cut
168              
169 1     1 1 19 async sub buffer_request_body {
170 1         3 my ($self, $receive) = @_;
171              
172 1         2 my $body = '';
173 1         2 my $event;
174              
175 1         1 while (1) {
176 2         3 $event = await $receive->();
177              
178 2 50       88 if ($event->{type} eq 'http.request') {
    0          
179 2   50     6 $body .= $event->{body} // '';
180 2 100       5 last unless $event->{more};
181             } elsif ($event->{type} eq 'http.disconnect') {
182 0         0 last;
183             }
184             }
185              
186 1         3 return ($body, $event);
187             }
188              
189             =head2 call
190              
191             await $middleware->call($scope, $receive, $send, $app);
192              
193             Convenience method to invoke the middleware with an app.
194             Equivalent to:
195              
196             my $wrapped = $middleware->wrap($app);
197             await $wrapped->($scope, $receive, $send);
198              
199             =cut
200              
201 0     0 1   async sub call {
202 0           my ($self, $scope, $receive, $send, $app) = @_;
203              
204 0           my $wrapped = $self->wrap($app);
205 0           await $wrapped->($scope, $receive, $send);
206             }
207              
208             1;
209              
210             __END__