File Coverage

blib/lib/Path/AttrRouter.pm
Criterion Covered Total %
statement 163 192 84.9
branch 33 68 48.5
condition 7 18 38.8
subroutine 20 24 83.3
pod 6 6 100.0
total 229 308 74.3


line stmt bran cond sub pod time code
1             package Path::AttrRouter;
2 8     8   230086 use Mouse;
  8         762419  
  8         52  
3              
4 8     8   2791 use Carp;
  8         18  
  8         657  
5 8     8   4917 use Path::AttrRouter::Controller;
  8         602  
  8         32  
6 8     8   3711 use Path::AttrRouter::Action;
  8         578  
  8         220  
7 8     8   3815 use Path::AttrRouter::Match;
  8         589  
  8         215  
8 8     8   6995 use Try::Tiny;
  8         11542  
  8         2064  
9              
10             our $VERSION = '0.04';
11              
12             has search_path => (
13             is => 'ro',
14             isa => 'Str',
15             required => 1,
16             );
17              
18             has actions => (
19             is => 'rw',
20             isa => 'HashRef',
21             default => sub { {} },
22             );
23              
24             has action_class => (
25             is => 'rw',
26             isa => 'Str',
27             default => 'Path::AttrRouter::Action',
28             );
29              
30             has action_cache => (
31             is => 'rw',
32             isa => 'Str',
33             );
34              
35             has dispatch_types => (
36             is => 'rw',
37             isa => 'ArrayRef',
38             lazy => 1,
39             default => sub {
40             my $self = shift;
41              
42             my @types;
43             for (qw/Path Regex Chained/) {
44             my $class = "Path::AttrRouter::DispatchType::$_";
45             $self->_ensure_class_loaded($class);
46             push @types, $class->new;
47             }
48              
49             \@types;
50             },
51             );
52              
53             has routing_table => (
54             is => 'rw',
55             isa => 'Object',
56             lazy => 1,
57             default => sub {
58             my $self = shift;
59             $self->_ensure_class_loaded('Path::AttrRouter::AsciiTable');
60             Path::AttrRouter::AsciiTable->new( router => $self );
61             },
62             );
63              
64 8     8   54 no Mouse;
  8         14  
  8         46  
65              
66             sub BUILD {
67 8     8 1 19 my $self = shift;
68              
69 8         50 $self->_ensure_class_loaded($self->action_class);
70              
71 8 100       185 if (my $cache_file = $self->action_cache) {
72 2         8 $self->_load_cached_modules($cache_file);
73             }
74             else {
75 6         26 $self->_load_modules;
76             }
77             }
78              
79             sub match {
80 59     59 1 50942 my ($self, $path, $condition) = @_;
81              
82 59         237 my @path = split m!/!, $path;
83 59 100       182 unshift @path, '' unless @path;
84              
85 59         80 my ($action, @args, @captures);
86             DESCEND:
87 59         134 while (@path) {
88 105         275 my $p = join '/', @path;
89 105         374 $p =~ s!^/!!;
90              
91 105         142 for my $type (@{ $self->dispatch_types }) {
  105         408  
92 227 50       1787 $action = $type->match({
93             path => $p,
94             args => \@args,
95             captures => \@captures,
96             action_class => $self->action_class,
97             $condition ? (%$condition) : (),
98             });
99 227 100       889 last DESCEND if $action;
100             }
101              
102 49         89 my $arg = pop @path;
103 49         79 $arg =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
  45         126  
104 49         155 unshift @args, $arg;
105             }
106              
107 45         128 s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg
108 59         165 for grep {defined} @captures;
  22         92  
109              
110 59 100       145 if ($action) {
111             # recreate controller instance if it is cached object
112 56 100       211 unless (ref $action->controller) {
113 12         39 $action->controller($self->_load_module($action->controller));
114 12         16 for my $act (@{ $action->chain }) {
  12         47  
115 0         0 $act->controller($self->_load_module($act->controller));
116             }
117             }
118              
119 56         759 return Path::AttrRouter::Match->new(
120             action => $action,
121             args => \@args,
122             captures => \@captures,
123             router => $self,
124             );
125             }
126 3         11 return;
127             }
128              
129             sub print_table {
130 0     0 1 0 print shift->routing_table->draw;
131             }
132              
133             sub get_action {
134 0     0 1 0 my ($self, $name, $namespace) = @_;
135 0 0       0 return unless $name;
136              
137 0   0     0 $namespace ||= '';
138 0 0       0 $namespace = '' if $namespace eq '/';
139              
140 0 0       0 my $container = $self->actions->{ $namespace } or return;
141 0 0       0 my $action = $container->{ $name } or return;
142              
143 0 0       0 $action->controller( $self->_load_module($action->controller) )
144             unless ref $action->controller;
145              
146 0         0 $action;
147             }
148              
149             sub get_actions {
150 0     0 1 0 my ($self, $action, $namespace) = @_;
151 0 0       0 return () unless $action;
152              
153 0         0 my @actions = grep { defined } map { $_->{ $action } } $self->_get_action_containers($namespace);
  0         0  
  0         0  
154 0         0 $_->controller( $self->_load_module($_->controller) )
155 0         0 for grep { !ref $_->controller } @actions;
156              
157 0         0 @actions;
158             }
159              
160             sub _get_action_containers {
161 0     0   0 my ($self, $namespace) = @_;
162 0   0     0 $namespace ||= '';
163 0 0       0 $namespace = '' if $namespace eq '/';
164              
165 0         0 my @containers;
166 0 0       0 if (length $namespace) {
167 0         0 do {
168 0         0 my $container = $self->actions->{ $namespace };
169 0 0       0 push @containers, $container if $container;
170             } while $namespace =~ s!/[^/]+$!!;
171             }
172 0 0       0 push @containers, $self->actions->{''} if $self->actions->{''};
173              
174 0         0 reverse @containers;
175             }
176              
177             sub make_action_cache {
178 1     1 1 2 my ($self, $file) = @_;
179              
180 1         2 my $used_dispatch_types = [grep { $_->used } @{ $self->dispatch_types }];
  3         10  
  1         4  
181              
182             # decompile regexp action because storable doen't recognize compiled regexp
183 1         2 my ($regex_type) = grep { $_->name eq 'Regex' } @{ $self->dispatch_types };
  3         11  
  1         4  
184 1 50       5 if ($regex_type->used) {
185 1         2 for my $compiled (@{ $regex_type->compiled }) {
  1         4  
186 1         7 $compiled->{re} = "$compiled->{re}";
187             }
188             }
189              
190 1         2 for my $namespace (keys %{ $self->actions }) {
  1         5  
191 3         8 my $container = $self->actions->{ $namespace };
192 3 50       5 for my $name (keys %{ $container || {} }) {
  3         12  
193 6         7 my $action = $container->{$name};
194 6         30 $action->{controller} = ref $action->{controller};
195             }
196             }
197              
198 1         6 my $cache = {
199             dispatch_types => $used_dispatch_types,
200             actions => $self->actions,
201             };
202              
203 1         6 Storable::store($cache, $file);
204             }
205              
206             sub _load_modules {
207 7     7   74 my ($self) = @_;
208              
209             # search on-memory modules
210 7         54 my @modules = $self->_search_loaded_classes($self->search_path);
211              
212             # search unload modules
213 7         23 $self->_ensure_class_loaded('Module::Pluggable::Object');
214 7         68883 my $finder = Module::Pluggable::Object->new(search_path => $self->search_path);
215 7         102 push @modules, $finder->plugins;
216              
217             # root module
218 7         5718 (my $root_class = $self->search_path) =~ s/::$//;
219 7 50   7   75 unshift @modules, $root_class if try { $self->_ensure_class_loaded($root_class) };
  7         180  
220              
221             # uniquify
222 7         231 @modules = do {
223 7         13 my %found;
224 7         54 $found{$_}++ for @modules;
225 7         39 keys %found;
226             };
227              
228 7         31 my $root = $self->search_path;
229 7         19 for my $module (@modules) {
230 17         65 my $controller = $self->_load_module($module);
231 17         65 $self->_register($controller);
232             }
233             }
234              
235             sub _load_module {
236 29     29   59 my ($self, $module) = @_;
237              
238 29         91 my $root = $self->search_path;
239 29         70 $self->_ensure_class_loaded($module);
240              
241 29         786 (my $namespace = $module) =~ s/^$root(?:::)?//;
242 29         68 $namespace =~ s!::!/!g;
243              
244 29 100       210 if (my $cache = $self->{__object_cache}{$module}) {
245 9         49 return $cache;
246             }
247             else {
248 20         332 my $controller = $module->new;
249 20 50       2112 $controller->namespace(lc $namespace) unless defined $controller->namespace;
250 20         118 return $self->{__object_cache}{$module} = $controller;
251             }
252             }
253              
254             sub _load_cached_modules {
255 2     2   5 my ($self, $cache_file) = @_;
256              
257 2         6 $self->_ensure_class_loaded('Storable');
258              
259 2     2   4468 my $cache = try { Storable::retrieve($cache_file) };
  2         53  
260              
261 2 100       3027 unless ($cache) {
262             # load modules and fill cache
263 1         6 $self->_load_modules;
264 1         6 $self->make_action_cache($cache_file);
265 1         425 return;
266             }
267              
268 1 50       3 $self->_ensure_class_loaded(ref $_) for @{ $cache->{dispatch_types} || [] };
  1         10  
269 1         17 $self->dispatch_types($cache->{dispatch_types});
270 1         9 $self->actions($cache->{actions});
271             }
272              
273             sub _register {
274 17     17   35 my ($self, $controller) = @_;
275 17   33     62 my $context_class = ref $controller || $controller;
276              
277 17         28 $controller->_method_cache([ @{$controller->_method_cache} ]);
  17         183  
278              
279 17         726 $self->_ensure_class_loaded('Data::Util');
280 17 50       13826 while (my $attr = shift @{ $controller->_attr_cache || [] }) {
  69         532  
281 52         616 my ($pkg, $method) = Data::Util::get_code_info($attr->[0]);
282 52         71 push @{ $controller->_method_cache }, [ $method, $attr->[1] ];
  52         135  
283             }
284              
285 17 50       140 for my $cache (@{ $controller->_method_cache || [] }) {
  17         52  
286 52         210 my ($method, $attrs) = @$cache;
287 52         232 $attrs = $self->_parse_action_attrs( $controller, $method, @$attrs );
288              
289 52         152 my $ns = $controller->namespace;
290 52 100       131 my $reverse = $ns ? "${ns}/${method}" : $method;
291              
292 52         938 my $action = $self->action_class->new(
293             name => $method,
294             reverse => $reverse,
295             namespace => $ns,
296             attributes => $attrs,
297             controller => $controller,
298             );
299 52         205 $self->_register_action($action);
300             }
301             }
302              
303             sub _register_action {
304 52     52   76 my ($self, $action) = @_;
305              
306 52         53 for my $type (@{ $self->dispatch_types }) {
  52         176  
307 156         462 $type->register($action);
308             }
309              
310 52   100     358 my $container = $self->actions->{ $action->namespace } ||= {};
311 52         476 $container->{ $action->name } = $action;
312             }
313              
314             # synbol table walking code from Mouse::Util
315             sub _search_loaded_classes {
316 16     16   30 my ($self, $path) = @_;
317             # walk the symbol table tree to avoid autovififying
318             # \*{${main::}{"Foo::"}} == \*main::Foo::
319              
320 16         18 my @found;
321 16         52 $path =~ s/::$//;
322              
323 16         30 my $pack = \%::;
324 16         60 for my $part (split '::', $path) {
325 29         67 my $entry = \$pack->{ $part . '::' };
326 29 50       94 return @found if ref $entry ne 'GLOB';
327 29 50       30 $pack = *{$entry}{HASH} or return @found;
  29         113  
328             }
329              
330 16 50 33     140 if (exists $pack->{ISA} and my $isa = $pack->{ISA}) {
331 16 50 33     21 if (defined *{$isa}{ARRAY} and @$isa != 0) {
  16         121  
332 16         29 (my $module = $path) =~ s/::$//;
333 16         36 push @found, $module;
334             }
335             }
336              
337 16         185 for my $submodule (grep /.+::$/, keys %$pack) {
338 9         58 push @found, $self->_search_loaded_classes($path . '::' . $submodule);
339             }
340              
341 16         70 return @found;
342             }
343              
344             sub _parse_action_attrs {
345 52     52   226 my ($self, $controller, $name, @attrs) = @_;
346              
347 52         65 my %parsed;
348 52         80 for my $attr (@attrs) {
349 95 50       779 if (my ($k, $v) = ( $attr =~ /^(.*?)(?:\(\s*(.+?)\s*\))?$/ )) {
350 95 100 66     449 ( $v =~ s/^'(.*)'$/$1/ ) || ( $v =~ s/^"(.*)"/$1/ )
351             if defined $v;
352              
353 95         186 my $initializer = "_parse_${k}_attr";
354 95 50       998 if ($controller->can($initializer)) {
355 95 50       357 ($k, $v) = $controller->$initializer($name, $v)
356             or next;
357 95         142 push @{ $parsed{$k} }, $v;
  95         401  
358             }
359             else {
360 0         0 carp qq[Unsupported attribute "${k}". ignored];
361             }
362             }
363             }
364              
365 52         153 return \%parsed;
366             }
367              
368             sub _ensure_class_loaded {
369 93     93   182 my ($self, $class) = @_;
370 93         293 Mouse::load_class($class);
371             }
372              
373             __PACKAGE__->meta->make_immutable;
374              
375             __END__