File Coverage

blib/lib/PAGI/Endpoint/Router.pm
Criterion Covered Total %
statement 128 167 76.6
branch 17 28 60.7
condition 4 6 66.6
subroutine 32 44 72.7
pod 3 4 75.0
total 184 249 73.9


line stmt bran cond sub pod time code
1             package PAGI::Endpoint::Router;
2              
3 2     2   158063 use strict;
  2         5  
  2         145  
4 2     2   10 use warnings;
  2         5  
  2         123  
5              
6 2     2   12 use Future::AsyncAwait;
  2         3  
  2         14  
7 2     2   125 use Carp qw(croak);
  2         3  
  2         152  
8 2     2   8 use Scalar::Util qw(blessed);
  2         3  
  2         70  
9 2     2   1080 use Module::Load qw(load);
  2         3276  
  2         14  
10              
11              
12             sub new {
13 10     10 0 186190 my ($class, %args) = @_;
14 10         37 return bless {
15             _state => {},
16             }, $class;
17             }
18              
19             # Worker-local state (NOT shared across workers)
20             sub state {
21 5     5 1 22 my ($self) = @_;
22 5         24 return $self->{_state};
23             }
24              
25             # Override in subclass to define routes
26             sub routes {
27 1     1 1 7 my ($self, $r) = @_;
28             # Default: no routes
29             }
30              
31              
32             sub to_app {
33 9     9 1 12332 my ($class) = @_;
34              
35             # Create instance that lives for app lifetime
36 9 100       50 my $instance = blessed($class) ? $class : $class->new;
37              
38             # Build internal router
39 9         31 load('PAGI::App::Router');
40 9         485 my $internal_router = PAGI::App::Router->new;
41              
42             # Let subclass define routes
43 9         53 $instance->_build_routes($internal_router);
44              
45 9         22 my $app = $internal_router->to_app;
46 9         32 my $state = $instance->{_state};
47              
48 18     18   2806 return async sub {
49 18         28 my ($scope, $receive, $send) = @_;
50              
51             # Inject instance state into scope (allows $req->state to work)
52 18   66     60 $scope->{state} //= $state;
53              
54             # Dispatch to internal router
55 18         58 await $app->($scope, $receive, $send);
56 9         48 };
57             }
58              
59             sub _build_routes {
60 9     9   14 my ($self, $r) = @_;
61              
62             # Create a wrapper router that intercepts route registration
63 9         34 my $wrapper = PAGI::Endpoint::Router::RouteBuilder->new($self, $r);
64 9         29 $self->routes($wrapper);
65             }
66              
67             # Internal route builder that wraps handlers
68             package PAGI::Endpoint::Router::RouteBuilder;
69              
70 2     2   912 use strict;
  2         3  
  2         38  
71 2     2   7 use warnings;
  2         2  
  2         74  
72 2     2   7 use Future::AsyncAwait;
  2         3  
  2         10  
73 2     2   101 use Scalar::Util qw(blessed);
  2         2  
  2         4389  
74              
75             sub new {
76 9     9   17 my ($class, $endpoint, $router) = @_;
77 9         25 return bless {
78             endpoint => $endpoint,
79             router => $router,
80             }, $class;
81             }
82              
83             # HTTP methods
84 11     11   45 sub get { shift->_add_http_route('GET', @_) }
85 1     1   3 sub post { shift->_add_http_route('POST', @_) }
86 0     0   0 sub put { shift->_add_http_route('PUT', @_) }
87 0     0   0 sub patch { shift->_add_http_route('PATCH', @_) }
88 0     0   0 sub delete { shift->_add_http_route('DELETE', @_) }
89 0     0   0 sub head { shift->_add_http_route('HEAD', @_) }
90 0     0   0 sub options { shift->_add_http_route('OPTIONS', @_) }
91              
92             sub _add_http_route {
93 12     12   26 my ($self, $method, $path, @rest) = @_;
94              
95 12         26 my ($middleware, $handler) = $self->_parse_route_args(@rest);
96              
97             # Wrap middleware
98 12         22 my @wrapped_mw = map { $self->_wrap_middleware($_) } @$middleware;
  4         8  
99              
100             # Wrap handler
101 12         25 my $wrapped = $self->_wrap_http_handler($handler);
102              
103             # Register with internal router using the appropriate HTTP method
104 12         17 my $router_method = lc($method);
105 12 100       43 $self->{router}->$router_method($path, @wrapped_mw ? (\@wrapped_mw, $wrapped) : $wrapped);
106              
107 12         37 return $self;
108             }
109              
110             sub _parse_route_args {
111 16     16   33 my ($self, @args) = @_;
112              
113 16 100 66     56 if (@args == 2 && ref($args[0]) eq 'ARRAY') {
    50          
114 3         8 return ($args[0], $args[1]);
115             }
116             elsif (@args == 1) {
117 13         29 return ([], $args[0]);
118             }
119             else {
120 0         0 die "Invalid route arguments";
121             }
122             }
123              
124             sub _wrap_http_handler {
125 12     12   14 my ($self, $handler) = @_;
126              
127 12         22 my $endpoint = $self->{endpoint};
128              
129             # If handler is a string, it's a method name
130 12 50       25 if (!ref($handler)) {
131 12         13 my $method_name = $handler;
132 12 50       62 my $method = $endpoint->can($method_name)
133             or die "No such method: $method_name in " . ref($endpoint);
134              
135 10     10   14 return async sub {
136 10         15 my ($scope, $receive, $send) = @_;
137              
138 10         1306 require PAGI::Request;
139 10         1674 require PAGI::Response;
140              
141 10         48 my $req = PAGI::Request->new($scope, $receive);
142 10         223 my $res = PAGI::Response->new($scope, $send);
143              
144 10         108 await $endpoint->$method($req, $res);
145 12         50 };
146             }
147              
148             # Already a coderef - wrap it
149 0     0   0 return async sub {
150 0         0 my ($scope, $receive, $send) = @_;
151              
152 0         0 require PAGI::Request;
153 0         0 require PAGI::Response;
154              
155 0         0 my $req = PAGI::Request->new($scope, $receive);
156 0         0 my $res = PAGI::Response->new($scope, $send);
157              
158 0         0 await $handler->($req, $res);
159 0         0 };
160             }
161              
162             sub websocket {
163 2     2   24 my ($self, $path, @rest) = @_;
164              
165 2         7 my ($middleware, $handler) = $self->_parse_route_args(@rest);
166 2         5 my @wrapped_mw = map { $self->_wrap_middleware($_) } @$middleware;
  0         0  
167 2         6 my $wrapped = $self->_wrap_websocket_handler($handler);
168              
169 2 50       11 $self->{router}->websocket($path, @wrapped_mw ? (\@wrapped_mw, $wrapped) : $wrapped);
170              
171 2         6 return $self;
172             }
173              
174             sub _wrap_websocket_handler {
175 2     2   5 my ($self, $handler) = @_;
176              
177 2         4 my $endpoint = $self->{endpoint};
178              
179 2 50       5 if (!ref($handler)) {
180 2         4 my $method_name = $handler;
181 2 50       11 my $method = $endpoint->can($method_name)
182             or die "No such method: $method_name";
183              
184 2     2   4 return async sub {
185 2         5 my ($scope, $receive, $send) = @_;
186              
187 2         1343 require PAGI::WebSocket;
188              
189 2         22 my $ws = PAGI::WebSocket->new($scope, $receive, $send);
190              
191 2         11 await $endpoint->$method($ws);
192 2         9 };
193             }
194              
195 0     0   0 return async sub {
196 0         0 my ($scope, $receive, $send) = @_;
197              
198 0         0 require PAGI::WebSocket;
199              
200 0         0 my $ws = PAGI::WebSocket->new($scope, $receive, $send);
201              
202 0         0 await $handler->($ws);
203 0         0 };
204             }
205              
206             sub sse {
207 2     2   9 my ($self, $path, @rest) = @_;
208              
209 2         6 my ($middleware, $handler) = $self->_parse_route_args(@rest);
210 2         5 my @wrapped_mw = map { $self->_wrap_middleware($_) } @$middleware;
  0         0  
211 2         6 my $wrapped = $self->_wrap_sse_handler($handler);
212              
213 2 50       9 $self->{router}->sse($path, @wrapped_mw ? (\@wrapped_mw, $wrapped) : $wrapped);
214              
215 2         5 return $self;
216             }
217              
218             sub _wrap_sse_handler {
219 2     2   4 my ($self, $handler) = @_;
220              
221 2         4 my $endpoint = $self->{endpoint};
222              
223 2 50       5 if (!ref($handler)) {
224 2         4 my $method_name = $handler;
225 2 50       11 my $method = $endpoint->can($method_name)
226             or die "No such method: $method_name";
227              
228 2     2   4 return async sub {
229 2         4 my ($scope, $receive, $send) = @_;
230              
231 2         1211 require PAGI::SSE;
232              
233 2         31 my $sse = PAGI::SSE->new($scope, $receive, $send);
234              
235 2         14 await $endpoint->$method($sse);
236 2         10 };
237             }
238              
239 0     0   0 return async sub {
240 0         0 my ($scope, $receive, $send) = @_;
241              
242 0         0 require PAGI::SSE;
243              
244 0         0 my $sse = PAGI::SSE->new($scope, $receive, $send);
245              
246 0         0 await $handler->($sse);
247 0         0 };
248             }
249              
250             sub _wrap_middleware {
251 4     4   22 my ($self, $mw) = @_;
252              
253 4         5 my $endpoint = $self->{endpoint};
254              
255             # String = method name
256 4 50       8 if (!ref($mw)) {
257 4 50       15 my $method = $endpoint->can($mw)
258             or die "No such middleware method: $mw";
259              
260 5     5   8 return async sub {
261 5         7 my ($scope, $receive, $send, $next) = @_;
262              
263 5         22 require PAGI::Request;
264 5         12 require PAGI::Response;
265              
266 5         12 my $req = PAGI::Request->new($scope, $receive);
267 5         14 my $res = PAGI::Response->new($scope, $send);
268              
269 5         12 await $endpoint->$method($req, $res, $next);
270 4         18 };
271             }
272              
273             # Already a coderef or object - pass through
274 0         0 return $mw;
275             }
276              
277             # Pass through mount to internal router
278             sub mount {
279 2     2   5 my ($self, @args) = @_;
280 2         7 $self->{router}->mount(@args);
281 2         4 return $self;
282             }
283              
284             # Pass through name() to internal router
285             sub name {
286 0     0     my ($self, $name) = @_;
287 0           $self->{router}->name($name);
288 0           return $self;
289             }
290              
291             # Pass through as() to internal router
292             sub as {
293 0     0     my ($self, $namespace) = @_;
294 0           $self->{router}->as($namespace);
295 0           return $self;
296             }
297              
298             # Pass through uri_for() to internal router
299             sub uri_for {
300 0     0     my ($self, @args) = @_;
301 0           return $self->{router}->uri_for(@args);
302             }
303              
304             # Pass through named_routes() to internal router
305             sub named_routes {
306 0     0     my ($self) = @_;
307 0           return $self->{router}->named_routes;
308             }
309              
310             1;
311              
312             __END__