File Coverage

blib/lib/Nile/Dispatcher.pm
Criterion Covered Total %
statement 6 104 5.7
branch 0 50 0.0
condition 0 34 0.0
subroutine 2 10 20.0
pod 0 4 0.0
total 8 202 3.9


line stmt bran cond sub pod time code
1             # Copyright Infomation
2             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
3             # Author : Dr. Ahmed Amin Elsheshtawy, Ph.D.
4             # Website: https://github.com/mewsoft/Nile, http://www.mewsoft.com
5             # Email : mewsoft@cpan.org, support@mewsoft.com
6             # Copyrights (c) 2014-2015 Mewsoft Corp. All rights reserved.
7             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
8             package Nile::Dispatcher;
9              
10             our $VERSION = '0.54';
11             our $AUTHORITY = 'cpan:MEWSOFT';
12              
13             =pod
14              
15             =encoding utf8
16              
17             =head1 NAME
18              
19             Nile::Dispatcher - Application action dispatcher.
20              
21             =head1 SYNOPSIS
22            
23             # dispatch the default route or detect route from request
24             $app->dispatcher->dispatch;
25              
26             # dispatch specific route and request method
27             $app->dispatcher->dispatch($route, $request_method);
28             $app->dispatcher->dispatch('/accounts/register/create');
29             $app->dispatcher->dispatch('/accounts/register/save', 'POST');
30              
31             =head1 DESCRIPTION
32              
33             Nile::Dispatcher - Application action dispatcher.
34              
35             =cut
36              
37 1     1   7 use Nile::Base;
  1         3  
  1         9  
38 1     1   5482 use Capture::Tiny ();
  1         2  
  1         1244  
39             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40             =head2 dispatch()
41            
42             # dispatch the default route or detect route from request
43             $app->dispatcher->dispatch;
44              
45             # dispatch specific route and request method
46             $app->dispatcher->dispatch($route, $request_method);
47              
48             Process the action and send output to client.
49              
50             =cut
51              
52             sub dispatch {
53              
54 0     0 0   my $self = shift;
55            
56 0           my $content = $self->dispatch_action(@_);
57              
58 0           return $content;
59              
60             }
61             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
62             =head2 dispatch_action()
63            
64             # dispatch the default route or detect route from request
65             $content = $app->dispatcher->dispatch_action;
66              
67             # dispatch specific route and request method
68             $content = $app->dispatcher->dispatch_action($route, $request_method);
69              
70             Process the action and return output.
71              
72             =cut
73              
74             sub dispatch_action {
75              
76 0     0 0   my ($self, $route, $request_method) = @_;
77            
78 0           my $app = $self->app;
79              
80 0   0       $request_method ||= $app->request->request_method;
81 0 0 0       $request_method ||= "ajax" if ($app->request->is_ajax);
82 0   0       $request_method ||= "*";
83              
84 0           $route = $self->route($route);
85 0   0       $route ||= "";
86              
87             # beginning slash. forum/topic => /forum/topic
88 0 0         $route = "/$route" if ($route !~ /^\//);
89              
90             #$match->{action}, $match->{args}, $match->{query}, $match->{uri}, $match->{code}, $match->{route}
91 0           my $match = $app->router->match($route, $request_method);
92             #$app->dump($match);
93            
94 0 0         if ($match->{action}) {
95 0           $route = $match->{action};
96 0           while (my($k, $v) = each %{$match->{args}}) {
  0            
97 0           $app->request->add_param($k, $v);
98             }
99             }
100             #------------------------------------------------------
101 0           my ($content, @result);
102 0           undef $@;
103             #------------------------------------------------------
104             # inline actions. $app->action("get", "/home", sub {...});
105             # inline actions. $app->capture("get", "/home", sub {...});
106 0 0         if (ref($route) eq "CODE") {
107 0 0 0       if (defined $match->{route}->{attributes} && $match->{route}->{attributes} =~ /capture/i) {
108             # run the action and capture output of print statements
109 0     0     ($content, @result) = Capture::Tiny::capture_merged {eval {$route->($self->app)}};
  0            
  0            
110             }
111 0 0 0       if (defined $match->{route}->{attributes} && $match->{route}->{attributes} =~ /command/i) {
112             # run the action and capture output of print statements
113 0     0     ($content, @result) = Capture::Tiny::capture_merged {eval {$route->($self->app)}};
  0            
  0            
114 0           $content .= join "", @result;
115             }
116             else {
117             # run the action and get the returned content
118 0           $content = eval {$route->($self->app)};
  0            
119             }
120              
121 0 0         if ($@) {
122 0           $app->abort("Dispatcher error. Inline action dispatcher error for route '$route'.\n\n$@");
123             }
124              
125 0           return $content;
126             }
127             #------------------------------------------------------
128             # if route is '/' then use the default route
129 0 0 0       if (!$route || $route eq "/") {
130 0           $route = $app->var->get("default_route");
131             }
132              
133 0   0       $route ||= $app->abort("Dispatcher error. No route defined.");
134            
135 0           my ($module, $controller, $action) = $self->action($route);
136              
137 0           my $class = "Nile::Module::${module}::${controller}";
138            
139 0           undef $@;
140 0           eval "use $class;";
141              
142 0 0         if ($@) {
143 0           $app->abort("Dispatcher error. Module error for route '$route' class '$class'.\n\n$@");
144             }
145            
146 0           my $object = $class->new();
147              
148 0 0         if (!$object->can($action)) {
149             # try /Accounts => Accounts/Accounts/Accounts
150 0 0 0       if (($module eq $controller) && ($action eq "index")) {
151             # try /Accounts => Accounts/Accounts/Accounts
152 0 0         if ($object->can($module)) {
    0          
153 0           $action = $module;
154             }
155             # try /Accounts => Accounts/Accounts/accounts
156             elsif ($object->can(lc($module))) {
157 0           $action = lc($module);
158             }
159             }
160             else {
161 0           $app->abort("Dispatcher error. Module '$class' action '$action' does not exist.");
162             }
163             }
164            
165 0           my $meta = $object->meta;
166            
167 0           my $attrs = $meta->get_method($action)->attributes;
168             #$app->dump($attrs);
169            
170             # sub home: Action Capture Public {...}
171 0 0         if (!grep(/^(action|capture|command|public)$/i, @$attrs)) {
172 0           $app->abort("Dispatcher error. Module '$class' method '$action' is not marked as 'Action' or 'Capture'.");
173             }
174              
175             #Methods: HEAD, POST, GET, PUT, DELETE, PATCH, [ajax]
176              
177 0 0 0       if ($request_method ne "*" && !grep(/^$request_method$/i, @$attrs)) {
178 0           $app->abort("Dispatcher error. Module '$class' action '$action' request method '$request_method' is not allowed.");
179             }
180            
181             # add method "me" or one of its alt
182 0           $app->add_object_context($object, $meta);
183            
184 0           undef $@;
185              
186 0 0         if (grep(/^(capture)$/i, @$attrs)) {
    0          
187             # run the action and capture output of print statements. sub home: Capture {...}
188 0     0     ($content, @result) = Capture::Tiny::capture_merged {eval {$object->$action($self->app)}};
  0            
  0            
189             }
190             elsif (grep(/^(command)$/i, @$attrs)) {
191             # run the action and capture output of print statements and return value. sub home: Command {...}
192 0     0     ($content, @result) = Capture::Tiny::capture_merged {eval {$object->$action($self->app)}};
  0            
  0            
193 0           $content .= join "", @result;
194             }
195             else {
196             # run the action and get the returned content sub home: Action {...}
197 0           $content = eval {$object->$action($self->app)};
  0            
198             }
199              
200 0 0         if ($@) {
201 0           $content = "Module error: Module '$class' method '$action'. $@\n$content\n";
202             }
203              
204 0           return $content;
205             }
206             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
207             =head2 action()
208            
209             my ($module, $controller, $action) = $app->dispatcher->action($route);
210             #route /module/controller/action returns (Module, Controller, action)
211             #route /module/action returns (Module, Module, action)
212             #route /module returns (Module, Module, index)
213              
214             Find the action module, controller and method name from the provided route.
215              
216             =cut
217              
218             sub action {
219              
220 0     0 0   my ($self, $route) = @_;
221              
222 0 0         $route || return;
223 0           my ($module, $controller, $action);
224            
225 0           $route =~ s/^\/+//;
226            
227 0           my @parts = split(/\//, $route);
228              
229 0 0         if (scalar @parts == 3) {
    0          
    0          
230 0           ($module, $controller, $action) = @parts;
231             }
232             elsif (scalar @parts == 2) {
233 0           $module = $parts[0];
234 0           $controller = $parts[0];
235 0           $action = $parts[1];
236             }
237             elsif (scalar @parts == 1) {
238 0           $module = $parts[0];
239 0           $controller = $parts[0];
240 0           $action = "index";
241             }
242            
243 0   0       $module ||= "";
244 0   0       $controller ||= "";
245            
246 0           return (ucfirst($module), ucfirst($controller), $action);
247             }
248             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
249             =head2 route()
250            
251             my $route = $app->dispatcher->route($route);
252            
253             Detects the current request path if not provided from the request params named as
254             'action', 'route', or 'cmd' in the post or get methods:
255            
256             # uri route
257             /blog/?action=register
258            
259             # form route
260             <input type="hidden" name="action" value="register" />
261              
262             If not found, it will try to detect the route from the request uri after the path part
263            
264             # assuming application path is /blog, so /register will be the route
265             /blog/register
266              
267             =cut
268              
269             sub route {
270 0     0 0   my ($self, $route) = @_;
271            
272 0           my $app = $self->app;
273              
274             # if no route, try to find route from the request param named by action_name
275 0 0         if (!$route) {
276             # allow multiple names separated with commas, i.e. 'action', 'action,route,cmd'.
277 0           my @action_name = split(/\,/, $app->var->get("action_name"));
278 0           foreach (@action_name) {
279 0 0         last if ($route = $app->request->param($_));
280             }
281             }
282            
283             # if no route, get the route from the query string in the REQUEST_URI
284 0   0       $route ||= $app->request->url_path;
285            
286 0 0         if ($route) {
287 0           $route =~ s!^/!!g;
288 0           $route =~ s!/$!!g;
289             }
290              
291 0           return $route;
292             }
293             #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
294              
295             =pod
296              
297             =head1 Bugs
298              
299             This project is available on github at L<https://github.com/mewsoft/Nile>.
300              
301             =head1 HOMEPAGE
302              
303             Please visit the project's homepage at L<https://metacpan.org/release/Nile>.
304              
305             =head1 SOURCE
306              
307             Source repository is at L<https://github.com/mewsoft/Nile>.
308              
309             =head1 SEE ALSO
310              
311             See L<Nile> for details about the complete framework.
312              
313             =head1 AUTHOR
314              
315             Ahmed Amin Elsheshtawy, احمد امين الششتاوى <mewsoft@cpan.org>
316             Website: http://www.mewsoft.com
317              
318             =head1 COPYRIGHT AND LICENSE
319              
320             Copyright (C) 2014-2015 by Dr. Ahmed Amin Elsheshtawy احمد امين الششتاوى mewsoft@cpan.org, support@mewsoft.com,
321             L<https://github.com/mewsoft/Nile>, L<http://www.mewsoft.com>
322              
323             This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
324              
325             =cut
326              
327             1;