File Coverage

blib/lib/Spike/Site/Router.pm
Criterion Covered Total %
statement 42 139 30.2
branch 0 40 0.0
condition 0 38 0.0
subroutine 14 27 51.8
pod 0 6 0.0
total 56 250 22.4


line stmt bran cond sub pod time code
1             package Spike::Site::Router;
2              
3 1     1   4 use strict;
  1         1  
  1         22  
4 1     1   3 use warnings;
  1         1  
  1         21  
5              
6 1     1   7 use base qw(Spike::Site::Handler);
  1         2  
  1         317  
7              
8 1     1   6 use Spike::Error;
  1         1  
  1         32  
9 1     1   349 use Spike::Site::Router::Route;
  1         2  
  1         18  
10 1     1   289 use Spike::Cache;
  1         2  
  1         18  
11              
12 1     1   3 use Carp;
  1         1  
  1         37  
13              
14 1     1   3 use HTTP::Status qw(:constants);
  1         2  
  1         254  
15 1     1   390 use Plack::MIME;
  1         521  
  1         27  
16              
17 1     1   4 use POSIX qw(strftime);
  1         1  
  1         5  
18 1     1   457 use Date::Parse;
  1         5383  
  1         129  
19              
20 1     1   6 use List::Util qw(first uniq);
  1         1  
  1         65  
21 1     1   559 use List::MoreUtils qw(zip);
  1         7831  
  1         8  
22 1     1   412 use Scalar::Util qw(blessed);
  1         1  
  1         1033  
23              
24 0   0 0 0   sub route { (shift->{route} ||= Spike::Site::Router::Route->new)->route(@_) }
25              
26             sub config {
27 0     0 0   my $self = shift;
28 0   0       my $config = $self->{config} ||= Spike::Config->new;
29              
30 0 0         return $config->(@_) if @_;
31 0           return $config;
32             }
33              
34             sub new {
35 0     0 0   my $proto = shift;
36 0   0       my $class = ref $proto || $proto;
37              
38 0           my $self = $class->SUPER::new;
39              
40 0           $self->startup;
41 0           $self->_init_config;
42              
43 0           return $self;
44             }
45              
46             sub cache {
47 0     0 0   my $self = shift;
48 0           my $config = $self->config->site_route_cache;
49              
50 0   0       return $self->{cache} ||= Spike::Cache->new(
51             name => 'Route cache',
52             debug => $self->debug,
53              
54             max_records => $config->max_records('int', 10240),
55             max_ttl => $config->max_ttl('int', 3600),
56             max_idle_time => $config->max_idle_time('int', 300),
57             purge_time => $config->purge_time('int', 300),
58             );
59             }
60              
61             sub _init_config {
62 0     0     my $self = shift;
63              
64 0           (my $site_name = lc ref $self) =~ s/::.*$//;
65 0           my $site_home = "$FindBin::Bin/..";
66              
67 0           my %defaults = (
68             site_name => $site_name,
69             config_path => "$site_home/conf",
70             template_path => "$site_home/tmpl/$site_name",
71             static_path => "$site_home/web/$site_name",
72             );
73              
74             $self->config->site->$_(set => $defaults{$_})
75 0           for keys %defaults;
76             }
77              
78             sub _find_handlers {
79 0     0     my ($self, $route) = @_;
80              
81 0           my (@routes, %errors, @names);
82              
83 0           while ($route) {
84 0           unshift @routes, $route;
85 0           unshift @names, $route->name;
86              
87 0           $route = $route->parent;
88             }
89              
90 0           shift @names; # exclude root
91              
92 0     0     my $prepare = sub { @_ };
  0            
93 0     0     my $finalize = sub {};
94              
95 0           for $route (@routes) {
96 0   0       $prepare = $route->prepare || $prepare;
97 0   0       $finalize = $route->finalize || $finalize;
98              
99             $errors{$_} = [ $route->error($_), $prepare, $finalize ]
100 0           for $route->errors;
101             }
102              
103 0           return $prepare, $finalize, \%errors, \@names;
104             }
105              
106             sub _try_static {
107 0     0     my ($self, $req, $res, $path) = @_;
108              
109 0 0         return if $path =~ m!(^|/)\.!;
110              
111 0 0         my @files = length $path ? ($path, "$path/index.html") : ("index.html");
112              
113 0           for my $file (@files) {
114 0           my $full_path = $self->config->site->static_path."/".$file;
115              
116 0 0 0       next unless -f $full_path && open my $fh, '<', $full_path;
117              
118 0 0         return 1 if $req->method ne 'GET';
119              
120 0           my ($size, $mtime) = (stat $fh)[7, 9];
121              
122 0           $res->header('Last-Modified' => strftime('%c', localtime $mtime));
123              
124 0     0     my $req_mtime = first { defined $_ } map { str2time($_) }
  0            
  0            
125             $req->header('If-Modified-Since');
126              
127 0 0 0       if (!$req_mtime || $req_mtime < $mtime) {
128 0   0       my $suffix = ($file =~ m!(\.[^/.]+)?$!)[0] || '.html';
129              
130 0           $res->content_length($size);
131 0           $res->content_type(Plack::MIME->mime_type($suffix));
132              
133 0           $res->body($fh);
134             }
135             else {
136 0           $res->status(HTTP_NOT_MODIFIED);
137             }
138              
139 0           throw Spike::Error::HTTP_OK;
140             }
141              
142 0           return;
143             }
144              
145             sub _handle_error {
146 0     0     my ($self, $req, $res, $error, $errors) = @_;
147              
148 0 0         $error = new Spike::Error::HTTP($error) if !ref $error;
149              
150 0           my $class = ref $error;
151 0           my $is_http = $error->isa('Spike::Error::HTTP');
152              
153 0   0       my $handlers = $errors->{$class} || ($is_http && $errors->{$error->value});
154              
155 0 0         throw $error if !$handlers;
156              
157 0 0         %$res = %{ $is_http ?
  0            
158             $req->new_response($error->value, $error->headers) :
159             $req->new_response(HTTP_INTERNAL_SERVER_ERROR)
160             };
161              
162 0           my ($handler, $prepare, $finalize) = @$handlers;
163              
164 0           $finalize->($req, $res,
165             $handler->($req, $res,
166             $prepare->($req, $res, $error)
167             )
168             );
169             }
170              
171             sub handler {
172 0     0 0   my ($self, $req, $res) = @_;
173              
174 0           my $path = $req->safe_path;
175              
176 0 0 0       my @methods = $self->debug &&
177             $self->_try_static($req, $res, $path) ? qw(GET) : ();
178              
179 0           my @handlers = $self->cache->get($path);
180              
181 0 0         if (!@handlers) {
182 0           my ($found, $last_found) = $self->route->find($path);
183              
184 0   0       @handlers = (
185             $found,
186             $self->_find_handlers($found || $last_found),
187             [ split m!/!, $path ],
188             );
189              
190 0           $self->cache->store($path, @handlers);
191             }
192              
193 0           my ($route, $prepare, $finalize, $errors, $names, $values) = @handlers;
194              
195 0           $req->_bind_named_url_parameters(zip @$names, @$values);
196              
197 0           my $handler;
198              
199 0 0         if ($route) {
200 0   0       $handler = $route->method($req->method) || $route->method;
201 0           push @methods, $route->methods;
202             }
203              
204 0 0         if (!$handler) {
205 0 0         if (@methods) {
206 0           $self->_handle_error($req, $res, new Spike::Error::HTTP(
207             HTTP_METHOD_NOT_ALLOWED, allow => join(', ', uniq @methods),
208             ), $errors);
209             }
210             else {
211 0           $self->_handle_error($req, $res, HTTP_NOT_FOUND, $errors);
212             }
213 0           return;
214             }
215              
216 0           eval {
217 0           $finalize->($req, $res,
218             $handler->($req, $res,
219             $prepare->($req, $res)
220             )
221             );
222             };
223 0 0         if (my $error = $@) {
224 0 0         if (blessed $error) {
225 0 0         if ($error->isa('Spike::Error::HTTP_OK')) {
    0          
    0          
226 0           throw $error;
227             }
228             elsif ($error->isa('Spike::Error::HTTP')) {
229 0           carp "HTTP error: status=".$error->value.", text=\"".$error->text."\"";
230 0           $self->_handle_error($req, $res, $error, $errors);
231             }
232             elsif ($error->isa('Spike::Error')) {
233 0           carp "Error: class=".ref($error).", text=\"".$error->text."\"";
234 0           $self->_handle_error($req, $res, $error, $errors);
235             }
236             else {
237 0 0         carp "Error: class=".ref($error).", text=\"".($error->can('text') ? $error->text : "$error")."\"";
238 0           $self->_handle_error($req, $res, HTTP_INTERNAL_SERVER_ERROR, $errors);
239             }
240             }
241             else {
242 0           carp $error;
243 0           $self->_handle_error($req, $res, HTTP_INTERNAL_SERVER_ERROR, $errors);
244             }
245             }
246             }
247              
248       0 0   sub startup {}
249              
250             1;