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   69 use v5.36;
  7         19  
2             package PlackX::Framework::Router::Engine {
3 7     7   30 use parent 'Router::Boom';
  7         11  
  7         35  
4 7     7   31829 use Role::Tiny::With;
  7         32179  
  7         6581  
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 105 sub instance ($class) { state %instances; $instances{$class} ||= $class->SUPER::new }
  78         86  
  78         86  
  78         77  
  78         309  
10              
11             # Matching object methods ###########################################
12 86     86 1 108 sub match ($self, $request) {
  86         92  
  86         90  
  86         107  
13 86 50 33     545 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         270 my $destination = sprintf('/[ %s ]%s', $request->method, $request->destination);
18 86 100       773 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         16160 my %match = $match_array[0]->%*;
24 62         197 my %captures = $match_array[1]->%*;
25              
26 62         117 delete $captures{PXF_REQUEST_METHOD}; # not needed anymore
27 62         140 $match{route_parameters} = \%captures;
28              
29             # Add global filters (the match already has local filters in it)
30 62         122 for my $filter_type (qw/prefilters postfilters/) {
31 124 100       247 if (my $filters = $self->_match_global_filters($filter_type, $request)) {
32 32   100     99 $match{$filter_type} ||= [];
33             # put global prefilters before local and global postfilters after local
34 32 100       58 unshift @{$match{$filter_type}}, @$filters if $filter_type eq 'prefilters';
  10         23  
35 32 100       90 push @{$match{$filter_type}}, @$filters if $filter_type eq 'postfilters';
  22         69  
36             }
37             }
38              
39 62         327 return \%match;
40             }
41              
42 124     124   154 sub _match_global_filters ($self, $kind, $request) {
  124         156  
  124         162  
  124         132  
  124         133  
43 124 50 66     345 die "invalid kind of filters: '$kind'"
44             unless $kind eq 'prefilters' or $kind eq 'postfilters';
45              
46             # Shortcut?
47 124 100       398 return unless defined $self->{"global_$kind"};
48              
49 32         63 my @matches = ();
50 32         71 foreach my $filter ($self->{"global_$kind"}->@*) {
51 72         244 my $pattern = $filter->{'pattern'};
52             push @matches, $filter->{action}
53 72 100 100     273 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         153 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 18 sub freeze ($self) {
  11         14  
  11         15  
64 11         65 $self->{pxf_frozen} = 1;
65 11         47 $self->regexp;
66              
67             die 'Router has no routes'
68 11 50 33     7986 unless ref $self->{leaves} eq 'ARRAY' and $self->{leaves}->@* > 0;
69              
70 11         27 return;
71             }
72              
73             # Override add to check for frozen-ness
74 44     44 1 53 sub add ($self, @params) {
  44         48  
  44         60  
  44         79  
75 44 50       84 die ref($self) . ': router is frozen' if $self->{pxf_frozen};
76 44         126 return $self->SUPER::add(@params);
77             }
78              
79             # Meta object methods - add route or global filter ##################
80 40     40 1 58 sub add_route ($self, %params) {
  40         46  
  40         159  
  40         50  
81 40         60 my $route = $params{spec}; # Keep in params for later debugging
82 40         92 my $base = $params{base}; # Keep in params to match URLs later
83              
84             # Validate % params
85 40 50 100     164 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       112 $route = { any => $route } unless ref $route eq 'HASH';
90 40         94 foreach my $key (keys %$route) {
91 41 100       150 my $paths = ref $route->{$key} ? $route->{$key} : [$route->{$key}];
92 41 100 66     130 my $verb = ($key and uc($key) ne 'ANY') ? uc $key : undef;
93 41         118 $self->add(_path_with_base_and_method($_, $base, $verb), \%params) for @$paths;
94             }
95             }
96              
97 8     8 1 12 sub add_global_filter ($self, %params) {
  8         13  
  8         20  
  8         9  
98 8         14 my $when = delete $params{when};
99 8         13 my $pattern = delete $params{pattern};
100 8         14 my $action = delete $params{action};
101              
102             # Validate subroutine params
103 8 50 66     25 die q/Usage: add_global_filter(when => 'before'|'after', ...)/
104             unless $when eq 'before' or $when eq 'after';
105              
106 8 100       17 my $hkey = $when eq 'before' ? 'global_prefilters' : 'global_postfilters';
107 8   100     51 $self->{$hkey} ||= [];
108 8         9 push @{$self->{$hkey}}, { pattern => $pattern, action => $action };
  8         38  
109             }
110              
111             # Helper functions ##################################################
112 44     44   45 sub _path_with_base ($path, $base) {
  44         48  
  44         48  
  44         54  
113 44 100 66     132 return $path unless $base and length $base;
114 10 50       25 $path = '/' . $path if substr($path, 0, 1) ne '/';
115 10         20 return $base . $path;
116             }
117              
118 44     44   45 sub _path_with_method ($path, $method = undef) {
  44         47  
  44         50  
  44         43  
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       81 $method = $method ? ":$method" : '';
124 44         115 $path = "/[ {PXF_REQUEST_METHOD$method} ]$path";
125 44         63 return $path;
126             }
127              
128 44     44   247 sub _path_with_base_and_method ($path, $base, $method = undef) {
  44         54  
  44         62  
  44         54  
  44         41  
129 44         74 $path = _path_with_base($path, $base);
130 44         95 $path = _path_with_method($path, $method);
131 44         121 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