File Coverage

blib/lib/PlackX/Framework/Router/Engine.pm
Criterion Covered Total %
statement 103 107 96.2
branch 31 38 81.5
condition 40 52 76.9
subroutine 13 14 92.8
pod 6 7 85.7
total 193 218 88.5


line stmt bran cond sub pod time code
1 7     7   97 use v5.36;
  7         30  
2             package PlackX::Framework::Router::Engine {
3 7     7   45 use parent 'Router::Boom';
  7         13  
  7         42  
4 7     7   49093 use Role::Tiny::With;
  7         61844  
  7         10431  
5             with 'PlackX::Framework::Role::RouterEngine';
6              
7             # We use a Hybrid Singleton (one instance per subclass)
8 0     0 1 0 sub new ($class) { $class->instance; }
  0         0  
  0         0  
  0         0  
9 78   66 78 1 161 sub instance ($class) { state %instances; $instances{$class} ||= $class->SUPER::new }
  78         133  
  78         118  
  78         107  
  78         478  
10              
11             # Matching object methods ###########################################
12 86     86 1 139 sub match ($self, $request) {
  86         147  
  86         144  
  86         131  
13 86 50 33     947 die 'Usage: $engine->match($plackx_framework_request)'
      33        
14             unless $request and ref $request and $request->can('destination');
15              
16             # Prefix with [ method ] to include in match
17 86         346 my $destination = sprintf('/[ %s ]%s', $request->method, $request->destination);
18 86 100       1201 my @match_array = $self->SUPER::match($destination)
19             or return undef;
20              
21             # Note Router::Boom can return the same references multiple times, so copy
22             # This caused some very strange bugs in development!
23 62         24517 my %match = $match_array[0]->%*;
24 62         246 my %captures = $match_array[1]->%*;
25              
26 62         159 delete $captures{PXF_REQUEST_METHOD}; # not needed anymore
27 62         232 $match{route_parameters} = \%captures;
28              
29             # Add global filters (the match already has local filters in it)
30 62         164 for my $filter_type (qw/prefilters postfilters/) {
31 124 100       379 if (my $filters = $self->_match_global_filters($filter_type, $request)) {
32 32   100     179 $match{$filter_type} ||= [];
33             # put global prefilters before local and global postfilters after local
34 32 100       111 unshift @{$match{$filter_type}}, @$filters if $filter_type eq 'prefilters';
  10         33  
35 32 100       98 push @{$match{$filter_type}}, @$filters if $filter_type eq 'postfilters';
  22         93  
36             }
37             }
38              
39 62         488 return \%match;
40             }
41              
42 124     124   212 sub _match_global_filters ($self, $kind, $request) {
  124         195  
  124         221  
  124         176  
  124         219  
43 124 50 66     481 die "invalid kind of filters: '$kind'"
44             unless $kind eq 'prefilters' or $kind eq 'postfilters';
45              
46             # Shortcut?
47 124 100       605 return unless defined $self->{"global_$kind"};
48              
49 32         93 my @matches = ();
50 32         91 foreach my $filter ($self->{"global_$kind"}->@*) {
51 72         350 my $pattern = $filter->{'pattern'};
52             push @matches, $filter->{action}
53 72 100 100     411 if (!defined $pattern)
      100        
      100        
      100        
      100        
      100        
54             or (ref $pattern eq 'SCALAR' and $request->destination eq $$pattern)
55             or (ref $pattern eq 'Regexp' and $request->destination =~ $pattern)
56             or (!ref $pattern and substr($request->destination, 0, length $pattern) eq $pattern);
57             }
58 32         251 return \@matches;
59             }
60              
61             # Allow freezing of router engine so we can generate the regex in the parent
62             # in forked server types.
63 11     11 0 52 sub freeze ($self) {
  11         60  
  11         31  
64 11         27 $self->{pxf_frozen} = 1;
65 11         56 $self->regexp;
66              
67             die 'Router has no routes'
68 11 50 33     11849 unless ref $self->{leaves} eq 'ARRAY' and $self->{leaves}->@* > 0;
69              
70 11         31 return;
71             }
72              
73             # Override add to check for frozen-ness
74 44     44 1 73 sub add ($self, @params) {
  44         79  
  44         113  
  44         126  
75 44 50       144 die ref($self) . ': router is frozen' if $self->{pxf_frozen};
76 44         225 return $self->SUPER::add(@params);
77             }
78              
79             # Meta object methods - add route or global filter ##################
80 40     40 1 74 sub add_route ($self, %params) {
  40         82  
  40         227  
  40         74  
81 40         83 my $route = $params{spec}; # Keep in params for later debugging
82 40         97 my $base = $params{base}; # Keep in params to match URLs later
83              
84             # Validate % params
85 40 50 100     300 die 'add_route(spec => STRING|ARRAYREF|HASHREf)'
      66        
86             unless ref $route eq 'HASH' or ref $route eq 'ARRAY' or not ref $route;
87              
88             # Coerce to hashref { verb => path } or { verb => [paths] } and parse
89 40 100       238 $route = { any => $route } unless ref $route eq 'HASH';
90 40         141 foreach my $key (keys %$route) {
91 41 100       259 my $paths = ref $route->{$key} ? $route->{$key} : [$route->{$key}];
92 41 100 66     208 my $verb = ($key and uc($key) ne 'ANY') ? uc $key : undef;
93 41         145 $self->add(_path_with_base_and_method($_, $base, $verb), \%params) for @$paths;
94             }
95             }
96              
97 8     8 1 16 sub add_global_filter ($self, %params) {
  8         15  
  8         38  
  8         14  
98 8         17 my $when = delete $params{when};
99 8         18 my $pattern = delete $params{pattern};
100 8         17 my $action = delete $params{action};
101              
102             # Validate subroutine params
103 8 50 66     32 die q/Usage: add_global_filter(when => 'before'|'after', ...)/
104             unless $when eq 'before' or $when eq 'after';
105              
106 8 100       24 my $hkey = $when eq 'before' ? 'global_prefilters' : 'global_postfilters';
107 8   100     38 $self->{$hkey} ||= [];
108 8         13 push @{$self->{$hkey}}, { pattern => $pattern, action => $action };
  8         64  
109             }
110              
111             # Helper functions ##################################################
112 44     44   72 sub _path_with_base ($path, $base) {
  44         73  
  44         75  
  44         73  
113 44 100 66     209 return $path unless $base and length $base;
114 10 50       31 $path = '/' . $path if substr($path, 0, 1) ne '/';
115 10         33 return $base . $path;
116             }
117              
118 44     44   81 sub _path_with_method ($path, $method = undef) {
  44         77  
  44         75  
  44         67  
119             # $method is verb or verbs separated with pipe (e.g. 'get|post'), or undef
120             # A real request uri should never have [] or spaces in it, so use those to
121             # separate the method from the remaining uri. Thankfully Router::Boom does
122             # not check uris for validity, otherwise this would not work.
123 44 100       120 $method = $method ? ":$method" : '';
124 44         135 $path = "/[ {PXF_REQUEST_METHOD$method} ]$path";
125 44         149 return $path;
126             }
127              
128 44     44   436 sub _path_with_base_and_method ($path, $base, $method = undef) {
  44         78  
  44         74  
  44         83  
  44         69  
129 44         113 $path = _path_with_base($path, $base);
130 44         126 $path = _path_with_method($path, $method);
131 44         199 return $path;
132             }
133             }
134              
135             1;
136              
137             =pod
138              
139             =head1 NAME
140              
141             PlackX::Framework::Router::Engine
142              
143              
144             =head1 DESCRIPTION
145              
146             This module provides route and global filter matching for PlackX::Framework.
147             Please see PlackX::Framework and PlackX::Framework::Router for details of how
148             to use routing and filters in your application.
149              
150             =head2 Difference between Router and Router::Engine
151              
152             The difference between PXF's ::Router module and the ::Router::Engine is that
153             the Router module is primarily responsible for exporting the routing DSL
154             and processing calls to add routes and filters, while the Router::Engine class
155             is responsible for storing routes and matching routes against requests.
156              
157              
158             =head1 CLASS METHODS
159              
160             =over 4
161              
162             =item new, instance
163              
164             Return an object, creating a new one if one does not exist already.
165             This essentially allows each subclass to work as a singleton, so that there is
166             one PXF Router::Engine object per PXF application.
167              
168             =back
169              
170              
171             =head1 OBJECT METHODS
172              
173             =over 4
174              
175             =item add_route
176              
177             Adds a route. Please see documentation for PlackX::Framework::Router.
178             Do not add routes directly to the engine unless you wish to hack on the
179             framework.
180              
181             =item add_global_filter
182              
183             Adds a global filter. Please see documentation for PlackX::Framework::Router.
184             Do not add filters directly to the engine unless you wish to hack on the
185             framework.
186              
187             =item match
188              
189             Match a request against the route data. Returns the matched route and any
190             prefilters and postfilters that should be executed, or undef if no route
191             matches. The returned hashref contains the action to execute, filters
192             applicable to the route, and any parameters in the route, if applicable.
193              
194             {
195             action => CODEREF,
196             prefilters => ARRAYREF|undef,
197             postfilters => ARRAYREF|undef,
198             route_parameters => HASHREF
199             }
200              
201             Again, PlackX::Framework handles this for you, so there should be no need to
202             use this method directly.
203              
204             =back
205              
206              
207             =head1 META
208              
209             For author, copyright, and license, see PlackX::Framework.
210