File Coverage

blib/lib/PAGI/Endpoint/Router.pm
Criterion Covered Total %
statement 129 166 77.7
branch 17 28 60.7
condition 4 6 66.6
subroutine 33 45 73.3
pod 4 5 80.0
total 187 250 74.8


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