File Coverage

blib/lib/PAGI/Middleware.pm
Criterion Covered Total %
statement 34 35 97.1
branch 3 6 50.0
condition 2 4 50.0
subroutine 10 10 100.0
pod 5 5 100.0
total 54 60 90.0


line stmt bran cond sub pod time code
1             package PAGI::Middleware;
2             $PAGI::Middleware::VERSION = '0.002001';
3 26     26   229429 use strict;
  26         79  
  26         900  
4 26     26   135 use warnings;
  26         48  
  26         1068  
5 26     26   1879 use Future::AsyncAwait;
  26         67689  
  26         199  
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 205     205 1 4146187 my ($class, %config) = @_;
61              
62 205         538 my $self = bless {
63             config => \%config,
64             }, $class;
65 205         812 $self->_init(\%config);
66 203         438 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   44 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 46 my ($self, $app) = @_;
95              
96 1         11 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 149 my ($self, $scope, $additions) = @_;
127 50   50     101 $additions //= {};
128              
129 50         279 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 280 my ($self, $send, $interceptor) = @_;
153              
154 3     3   118 return async sub {
155 3         3 my ($event) = @_;
156 3         8 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 29 async sub buffer_request_body {
170 1         3 my ($self, $receive) = @_;
171              
172 1         3 my $body = '';
173 1         2 my $event;
174              
175 1         2 while (1) {
176 2         5 $event = await $receive->();
177              
178 2 50       104 if ($event->{type} eq 'http.request') {
    0          
179 2   50     8 $body .= $event->{body} // '';
180 2 100       7 last unless $event->{more};
181             } elsif ($event->{type} eq 'http.disconnect') {
182 0         0 last;
183             }
184             }
185              
186 1         5 return ($body, $event);
187             }
188              
189              
190             1;
191              
192             __END__