File Coverage

blib/lib/Puncheur.pm
Criterion Covered Total %
statement 190 271 70.1
branch 34 86 39.5
condition 17 41 41.4
subroutine 49 70 70.0
pod 7 35 20.0
total 297 503 59.0


line stmt bran cond sub pod time code
1             package Puncheur;
2 3     3   27986 use 5.010;
  3         9  
  3         104  
3 3     3   13 use strict;
  3         6  
  3         80  
4 3     3   14 use warnings;
  3         10  
  3         111  
5              
6             our $VERSION = "0.2.0";
7              
8 3     3   13 use Carp ();
  3         11  
  3         75  
9 3     3   4796 use Clone qw/clone/;
  3         13157  
  3         202  
10 3     3   2430 use Config::PL ();
  3         1888  
  3         60  
11 3     3   4408 use Encode;
  3         58038  
  3         314  
12 3     3   34 use File::Spec;
  3         5  
  3         76  
13 3     3   3041 use Plack::Session;
  3         5046  
  3         87  
14 3     3   2117 use Plack::Util;
  3         31554  
  3         85  
15 3     3   35 use Scalar::Util ();
  3         5  
  3         49  
16 3     3   2602 use URL::Encode;
  3         6528  
  3         137  
17              
18 3     3   2110 use Puncheur::Request;
  3         11  
  3         108  
19 3     3   1848 use Puncheur::Response;
  3         10  
  3         94  
20 3     3   1877 use Puncheur::Trigger qw/add_trigger call_trigger get_trigger_code/;
  3         9  
  3         1012  
21              
22             sub new {
23 2     2 1 504 my ($base_class, %args) = @_;
24 2 100       15 %args = (
25 2         5 %{ $base_class->setting || {} },
26             %args,
27             );
28              
29 2 100 66     17 if ($base_class eq __PACKAGE__ && !defined $args{app_name}) {
30 1         3 state $count = 0;
31 1         5 $args{app_name} = "Puncheur::_Sandbox" . $count++;
32             }
33              
34 2   66     12 my $class = $args{app_name} // $base_class;
35 2 100       9 if ($args{app_name}) {
36 1         2 local $@;
37 1         2 eval {
38 1         8 Plack::Util::load_class($class);
39 0 0       0 $class->import if $class->can('import');
40             };
41 1 50       576 if ($@) {
42 3     3   19 no strict 'refs'; @{"$class\::ISA"} = ($base_class);
  3         7  
  3         6420  
  1         3  
  1         31  
43             }
44 1 50       17 Carp::croak "$class is not $base_class class" unless $class->isa($base_class);
45             }
46 2         11 my $self = bless { %args }, $class;
47 2         19 $self->config; # surely assign config
48 2         12 $self;
49             }
50             our $_CONTEXT;
51 0     0 0 0 sub context { $_CONTEXT }
52              
53             my %_SETTING;
54             sub setting {
55 4     4 0 15 my ($class, %args) = @_;
56              
57 4 100       17 if (%args) {
58 2 50       8 Carp::croak qq[can't set class setting of $class] if $class eq __PACKAGE__;
59              
60 2 100       4 my %prev = %{ $_SETTING{$class} || {} };
  2         15  
61 2         9 $_SETTING{$class} = {
62             %prev,
63             %args,
64             };
65             }
66 4         29 $_SETTING{$class};
67             }
68              
69             # -------------------------------------------------------------------------
70             # Hook points:
71             # You can override them.
72 2     2 0 30 sub create_request { Puncheur::Request->new($_[1], $_[0]) }
73             sub create_response {
74 2     2 0 5 shift;
75 2         28 my $res = Puncheur::Response->new(@_);
76 2         267 $res->header( 'X-Content-Type-Options' => 'nosniff' );
77 2         115 $res->header( 'X-Frame-Options' => 'DENY' );
78 2         95 $res->header( 'Cache-Control' => 'private' );
79 2         81 $res;
80             }
81              
82             # -------------------------------------------------------------------------
83             # Application settings:
84             sub app_name {
85 9     9 1 17 my $self = shift;
86 9 100       42 ref $self || $self;
87             }
88              
89             sub asset_dir {
90 2     2 1 5 my $self = shift;
91              
92 2         4 my $asset_dir;
93 2 50 66     43 if (ref $self and $asset_dir = $self->{asset_dir}) {
    50          
94 0 0       0 $asset_dir = File::Spec->catfile($self->base_dir, $asset_dir)
95             unless File::Spec->file_name_is_absolute($asset_dir);
96             }
97             elsif ($self->can('share_dir')) {
98 2         11 $asset_dir = $self->share_dir;
99             }
100             else {
101 0         0 $asset_dir = File::Spec->catfile($self->base_dir, 'share');
102             }
103 2         23 $self->_cache_method($asset_dir);
104             }
105              
106             sub base_dir {
107 3     3 0 7 my $self = shift;
108 3         18 my $class = $self->app_name;
109              
110 3         8 my $base_dir = do {
111 3         7 my $path = $class;
112 3         10 $path =~ s!::!/!g;
113 3   100     20 my $app_name = ref $self && $self->{app_name};
114 3 100 66     24 if (!$app_name and my $libpath = $INC{"$path.pm"}) {
115 2         5 $libpath =~ s!\\!/!g; # win32
116 2 50       25 if ($libpath =~ s!(?:blib/)?lib/+$path\.pm$!!) {
117 0   0     0 File::Spec->rel2abs($libpath || './');
118             }
119             else {
120 2         55 File::Spec->rel2abs('./');
121             }
122             }
123             else {
124 1         42 File::Spec->rel2abs('./');
125             }
126             };
127 3         22 $class->_cache_method($base_dir);
128             }
129              
130 0     0 0 0 sub mode_name { $ENV{PLACK_ENV} }
131 1     1 0 13 sub debug_mode { $ENV{PUNCHEUR_DEBUG} }
132              
133             # you can override 2 methods below
134 1     1 0 14 sub html_content_type { 'text/html; charset=UTF-8' }
135 2     2 0 12 sub encoding { state $enc = Encode::find_encoding('utf-8') }
136              
137             # -------------------------------------------------------------------------
138             # view and render:
139             # You can override them
140             sub template_dir {
141 1     1 1 3 my $self = shift;
142 1         4 my $class = $self->app_name;
143              
144 1 50       6 my $tmpl = $self->{template_dir} ? $self->{template_dir} : File::Spec->catfile($self->asset_dir, 'tmpl');
145 1 50       5 my @tmpl = ref $tmpl ? @$tmpl : ($tmpl);
146              
147 2 0 33     159 @tmpl = map {
    50 0        
148 1         3 ref $_ && ref $_ eq 'CODE' ? $_->() :
149             ref $_ || File::Spec->file_name_is_absolute($_) ? $_ :
150             File::Spec->catfile($self->base_dir, $_)
151             } @tmpl;
152              
153 1         5 $self->_cache_method(\@tmpl);
154             }
155              
156             sub create_view {
157 1     1 0 3 my $self = shift;
158              
159             state $settings = {
160             MT => {
161             'Text::MicroTemplate::Extended' => {
162             include_path => $self->template_dir,
163             use_cache => 1,
164             macro => {
165 0     0   0 raw_string => sub($) { Text::MicroTemplate::EncodedString->new($_[0]) },
166 0     0   0 uri_for => sub { $self->context->uri_for(@_) },
167 0     0   0 uri_with => sub { $self->context->req->uri_with(@_) }
168             },
169             template_args => {
170 0     0   0 c => sub { $self->context },
171 0     0   0 s => sub { $self->context->stash },
172             }
173             },
174             },
175             Xslate => {
176             'Text::Xslate' => {
177             path => $self->template_dir,
178             module => [
179             'Text::Xslate::Bridge::Star',
180             ],
181             function => {
182 0     0   0 c => sub { $self->context },
183 0     0   0 uri_for => sub { $self->context->uri_for(@_) },
184 0     0   0 uri_with => sub { $self->context->req->uri_with(@_) }
185             },
186             ($self->debug_mode ? ( warn_handler => sub {
187 0     0   0 Text::Xslate->print( # print method escape html automatically
188             '[[', @_, ']]',
189             );
190 1 50       7 } ) : () ),
191             },
192             },
193             };
194              
195 1         4 my @args;
196 1 50       4 if (my $v = $self->{view}) {
197 0 0       0 @args = !ref $v ? %{ $settings->{$v} } : %$v;
  0         0  
198             }
199             else {
200 1         2 @args = %{ $settings->{Xslate} };
  1         11  
201             }
202              
203 1         996 require Tiffany;
204 1         350 my $view = Tiffany->load(@args);
205             }
206              
207             sub view {
208 1     1 1 3 my $self = shift;
209              
210 1         9 $self->_cache_method($self->create_view);
211             }
212              
213             sub render {
214 1     1 0 2 my $self = shift;
215 1         8 my $html = $self->view->render(@_);
216              
217 1         580 for my $code ($self->get_trigger_code('HTML_FILTER')) {
218 0         0 $html = $code->($self, $html);
219             }
220              
221 1         10 $html = Encode::encode($self->encoding, $html);
222 1         224 return $self->create_response(
223             200,
224             [
225             'Content-Type' => $self->html_content_type,
226             'Content-Length' => length($html)
227             ],
228             [$html],
229             );
230             }
231              
232             # -------------------------------------------------------------------------
233             # dispatcher and dispatch:
234             # You can override them
235             sub create_dispatcher {
236 0     0 0 0 my $self = shift;
237 0         0 my $class = $self->app_name;
238              
239 0         0 my $dispatcher_pkg = $class . '::Dispatcher';
240 0         0 local $@;
241 0         0 eval {
242 0         0 Plack::Util::load_class($dispatcher_pkg);
243 0 0       0 $dispatcher_pkg->import if $dispatcher_pkg->can('import');
244             };
245 0 0       0 if ($@) {
246 0   0     0 my $base_dispatcher = $self->{dispatcher} // 'PHPish';
247              
248 0         0 $base_dispatcher = Plack::Util::load_class($base_dispatcher, 'Puncheur::Dispatcher');
249 0 0       0 $base_dispatcher->import if $base_dispatcher->can('import');
250 3     3   24 no strict 'refs'; @{"$dispatcher_pkg\::ISA"} = ($base_dispatcher);
  3         5  
  3         1286  
  0         0  
  0         0  
251             }
252              
253 0 0       0 $dispatcher_pkg->can('new') ? $dispatcher_pkg->new($self) : $dispatcher_pkg;
254             }
255              
256             sub dispatcher {
257 0     0 1 0 my $self = shift;
258              
259 0         0 $self->_cache_method($self->create_dispatcher);
260             }
261              
262             sub dispatch {
263 0     0 0 0 my $self = shift;
264 0         0 $self->dispatcher->dispatch($self);
265             }
266              
267             # -------------------------------------------------------------------------
268             # Config:
269             # You can override them
270             sub load_config {
271 2     2 0 4 my $self = shift;
272              
273 2   33     78 my $config_file = $self->{config} || File::Spec->catfile('config', 'common.pl');
274 2 50       13 return $config_file if ref $config_file;
275 2 50       37 $config_file = File::Spec->catfile($self->base_dir, $config_file)
276             unless File::Spec->file_name_is_absolute($config_file);
277              
278 2 50       55 -e $config_file ? scalar Config::PL::config_do($config_file) : {};
279             }
280             sub config {
281 2     2 1 4 my $self = shift;
282              
283 2         16 $self->_cache_method($self->load_config);
284             }
285              
286             # -------------------------------------------------------------------------
287             # Util:
288             sub add_method {
289 8     8 0 20 my ($klass, $method, $code) = @_;
290 3     3   17 no strict 'refs';
  3         5  
  3         4558  
291 8         17 *{"${klass}::${method}"} = $code;
  8         54  
292             }
293              
294             sub _cache_method {
295 9     9   21733 my ($self, $stuff) = @_;
296 9 100       76 return $stuff unless ref $self; # don't cache in class method
297              
298 5         19 my $class = $self->app_name;
299              
300 5         51 my (undef, undef, undef, $sub) = caller(1);
301 5         30 $sub = +(split /::/, $sub)[-1];
302 5     2   33 my $code = sub { $stuff };
  2         49  
303 5         31 $class->add_method($sub, $code);
304 5         22 $stuff;
305             }
306              
307             # -------------------------------------------------------------------------
308             # Attributes:
309 5     5 0 38 sub request { $_[0]->{request} }
310 1     1 0 16 sub req { $_[0]->{request} }
311              
312             sub session {
313 4     4 0 8 my $c = shift;
314 4   66     33 $c->{session} ||= Plack::Session->new($c->request->env);
315             }
316              
317             sub stash {
318 0     0 0 0 my $c = shift;
319 0   0     0 $c->{stash} ||= {};
320             }
321              
322             # -------------------------------------------------------------------------
323             # Methods:
324             sub redirect {
325 0     0 0 0 my ($self, $location, $params) = @_;
326 0         0 my $url = do {
327 0 0       0 if ($location =~ m{^https?://}) {
328 0         0 $location;
329             }
330             else {
331 0         0 my $url = $self->req->base;
332 0         0 $url =~ s{/+$}{};
333 0         0 $location =~ s{^/+([^/])}{/$1};
334 0         0 $url .= $location;
335             }
336             };
337 0 0       0 if (my $ref = ref $params) {
338 0 0       0 my @ary = $ref eq 'ARRAY' ? @$params : %$params;
339 0         0 my $uri = URI->new($url);
340 0         0 $uri->query_form($uri->query_form, map { Encode::encode($self->encoding, $_) } @ary);
  0         0  
341 0         0 $url = $uri->as_string;
342              
343             }
344 0         0 return $self->create_response(
345             302,
346             ['Location' => $url],
347             []
348             );
349             }
350              
351             sub uri_for {
352 0     0 0 0 my ($self, $path, $query) = @_;
353 0   0     0 my $root = $self->req->base || '/';
354 0         0 $root =~ s{([^/])$}{$1/};
355 0         0 $path =~ s{^/}{};
356              
357 0 0       0 my @query = !$query ? () : ref $query eq 'HASH' ? %$query : @$query;
    0          
358 0         0 my @q;
359 0         0 while (my ($key, $val) = splice @query, 0, 2) {
360 0         0 $val = URL::Encode::url_encode(Encode::encode($self->encoding, $val));
361 0         0 push @q, "${key}=${val}";
362             }
363 0 0       0 $root . $path . (scalar @q ? '?' . join('&', @q) : '');
364             }
365              
366             # -------------------------------------------------------------------------
367             # PSGInise:
368             sub to_psgi {
369 1     1 0 2 my ($self, ) = @_;
370              
371 1 50       3 $self = $self->new unless ref $self;
372 1     2   7 return sub { $self->handle_request(shift) };
  2         20  
373             }
374 0     0 0 0 sub to_app { goto \&to_psgi }
375              
376             sub run {
377 0     0 0 0 my $self = shift;
378 0 0       0 my %opts = @_ == 1 ? %{$_[0]} : @_;
  0         0  
379              
380 0         0 my %server;
381 0         0 my $server = delete $opts{server};
382 0 0       0 $server{server} = $server if $server;
383              
384 0         0 my @options = %opts;
385 0         0 require Plack::Runner;
386              
387 0         0 my $runner = Plack::Runner->new(
388             %server,
389             options => \@options,
390             );
391 0         0 $runner->run($self->to_app);
392             }
393              
394             sub handle_request {
395 2     2 0 5 my ($self, $env) = @_;
396              
397 2         52 my $c = $self->clone;
398 2         15 $c->{request} = $c->create_request($env);
399              
400 2         31 local $_CONTEXT = $c;
401              
402 2         4 my $response;
403 2         18 for my $code ($c->get_trigger_code('BEFORE_DISPATCH')) {
404 0         0 $response = $code->($c);
405 0 0 0     0 goto PROCESS_END if Scalar::Util::blessed($response) && $response->isa('Plack::Response');
406             }
407 2 50       17 $response = $c->dispatch or die "cannot get any response";
408 2         18 PROCESS_END:
409             $c->call_trigger('AFTER_DISPATCH' => $response);
410              
411 2         13 return $response->finalize;
412             }
413              
414             # -------------------------------------------------------------------------
415             # Plugin
416             sub load_plugins {
417 1     1 0 4 my ($class, @args) = @_;
418 1         3 while (@args) {
419 2         4 my $module = shift @args;
420 2 50 66     29 my $conf = @args > 0 && ref $args[0] ? shift @args : undef;
421 2         7 $class->load_plugin($module, $conf);
422             }
423             }
424              
425             sub load_plugin {
426 3     3 0 8 my ($class, $module, $conf) = @_;
427              
428 3         15 $module = Plack::Util::load_class($module, 'Puncheur::Plugin');
429             {
430 3     3   50 no strict 'refs';
  3         5  
  3         138  
  3         26  
431 3         6 for my $method ( @{"${module}::EXPORT"} ){
  3         18  
432 3     3   14 use strict 'refs';
  3         5  
  3         792  
433 3         61 $class->add_method($method, $module->can($method));
434             }
435             }
436 3 50       43 $module->init($class, $conf) if $module->can('init');
437             }
438              
439             # -------------------------------------------------------------------------
440             # Raise Error:
441             my %StatusCode = (
442             400 => 'Bad Request',
443             401 => 'Unauthorized',
444             402 => 'Payment Required',
445             403 => 'Forbidden',
446             404 => 'Not Found',
447             405 => 'Method Not Allowed',
448             406 => 'Not Acceptable',
449             407 => 'Proxy Authentication Required',
450             408 => 'Request Timeout',
451             409 => 'Conflict',
452             410 => 'Gone',
453             411 => 'Length Required',
454             412 => 'Precondition Failed',
455             413 => 'Request Entity Too Large',
456             414 => 'Request-URI Too Large',
457             415 => 'Unsupported Media Type',
458             416 => 'Request Range Not Satisfiable',
459             417 => 'Expectation Failed',
460             418 => 'I\'m a teapot', # RFC 2324
461             422 => 'Unprocessable Entity', # RFC 2518 (WebDAV)
462             423 => 'Locked', # RFC 2518 (WebDAV)
463             424 => 'Failed Dependency', # RFC 2518 (WebDAV)
464             425 => 'No code', # WebDAV Advanced Collections
465             426 => 'Upgrade Required', # RFC 2817
466             428 => 'Precondition Required',
467             429 => 'Too Many Requests',
468             431 => 'Request Header Fields Too Large',
469             449 => 'Retry with', # unofficial Microsoft
470             500 => 'Internal Server Error',
471             501 => 'Not Implemented',
472             502 => 'Bad Gateway',
473             503 => 'Service Unavailable',
474             504 => 'Gateway Timeout',
475             505 => 'HTTP Version Not Supported',
476             506 => 'Variant Also Negotiates', # RFC 2295
477             507 => 'Insufficient Storage', # RFC 2518 (WebDAV)
478             509 => 'Bandwidth Limit Exceeded', # unofficial
479             510 => 'Not Extended', # RFC 2774
480             511 => 'Network Authentication Required',
481             );
482              
483             while ( my ($code, $msg) = each %StatusCode) {
484 3     3   17 no strict 'refs';
  3         7  
  3         157  
485             *{__PACKAGE__ ."::res_$code"} = sub {
486 3     3   15 use strict 'refs';
  3         21  
  3         586  
487 0     0     my $self = shift;
488 0           my $content = $self->error_html($code, $msg);
489 0           $self->create_response(
490             $code,
491             [
492             'Content-Type' => 'text/html; charset=utf-8',
493             'Content-Length' => length($content),
494             ],
495             [$content]
496             );
497             }
498             }
499              
500             # You can override it
501             sub error_html {
502 0     0 0   my ($self, $code, $msg) = @_;
503 0           sprintf q[
504            
505            
506            
507            
508            
509            
%s
510            
%s
511            
512             ], $code, $msg;
513             }
514              
515             1;
516             __END__