File Coverage

blib/lib/Puncheur.pm
Criterion Covered Total %
statement 193 281 68.6
branch 34 86 39.5
condition 17 41 41.4
subroutine 50 74 67.5
pod 7 36 19.4
total 301 518 58.1


line stmt bran cond sub pod time code
1             package Puncheur;
2 3     3   17370 use 5.010;
  3         8  
  3         105  
3 3     3   12 use strict;
  3         3  
  3         69  
4 3     3   8 use warnings;
  3         7  
  3         64  
5              
6 3     3   1227 use version 0.77; our $VERSION = version->declare("v0.3.0");
  3         4297  
  3         19  
7              
8 3     3   217 use Carp ();
  3         5  
  3         38  
9 3     3   1111 use Clone qw/clone/;
  3         6203  
  3         215  
10 3     3   1222 use Config::PL ();
  3         1353  
  3         49  
11 3     3   1670 use Encode;
  3         24340  
  3         206  
12 3     3   21 use File::Spec;
  3         5  
  3         60  
13 3     3   1191 use Plack::Session;
  3         2849  
  3         74  
14 3     3   920 use Plack::Util;
  3         18344  
  3         83  
15 3     3   20 use Scalar::Util ();
  3         5  
  3         46  
16 3     3   1287 use URL::Encode;
  3         3524  
  3         100  
17              
18 3     3   1077 use Puncheur::Request;
  3         11  
  3         99  
19 3     3   1246 use Puncheur::Response;
  3         10  
  3         87  
20 3     3   1041 use Puncheur::Trigger qw/add_trigger call_trigger get_trigger_code/;
  3         6  
  3         510  
21              
22             sub new {
23 2     2 1 389 my ($base_class, %args) = @_;
24 2 100       12 %args = (
25 2         4 %{ $base_class->setting || {} },
26             %args,
27             );
28              
29 2 100 66     13 if ($base_class eq __PACKAGE__ && !defined $args{app_name}) {
30 1         2 state $count = 0;
31 1         3 $args{app_name} = "Puncheur::_Sandbox" . $count++;
32             }
33              
34 2   66     11 my $class = $args{app_name} // $base_class;
35 2 100       8 if ($args{app_name}) {
36 1         1 local $@;
37 1         2 eval {
38 1         6 Plack::Util::load_class($class);
39 0 0       0 $class->import if $class->can('import');
40             };
41 1 50       226 if ($@) {
42 3     3   15 no strict 'refs'; @{"$class\::ISA"} = ($base_class);
  3         5  
  3         4025  
  1         2  
  1         15  
43             }
44 1 50       9 Carp::croak "$class is not $base_class class" unless $class->isa($base_class);
45             }
46 2         10 my $self = bless { %args }, $class;
47 2         13 $self->config; # surely assign config
48 2         10 $self;
49             }
50             our $_CONTEXT;
51 0     0 0 0 sub context { $_CONTEXT }
52              
53             my %_SETTING;
54             sub setting {
55 4     4 0 10 my ($class, %args) = @_;
56              
57 4 100       13 if (%args) {
58 2 50       6 Carp::croak qq[can't set class setting of $class] if $class eq __PACKAGE__;
59              
60 2 100       3 my %prev = %{ $_SETTING{$class} || {} };
  2         11  
61 2         8 $_SETTING{$class} = {
62             %prev,
63             %args,
64             };
65             }
66 4         26 $_SETTING{$class};
67             }
68              
69             # -------------------------------------------------------------------------
70             # Hook points:
71             # You can override them.
72 2     2 0 26 sub create_request { Puncheur::Request->new($_[1], $_[0]) }
73             sub create_response {
74 2     2 0 3 shift;
75 2         23 my $res = Puncheur::Response->new(@_);
76 2         216 $res->header( 'X-Content-Type-Options' => 'nosniff' );
77 2         86 $res->header( 'X-Frame-Options' => 'DENY' );
78 2         66 $res->header( 'Cache-Control' => 'private' );
79 2         57 $res;
80             }
81              
82             # -------------------------------------------------------------------------
83             # Application settings:
84             sub app_name {
85 9     9 1 13 my $self = shift;
86 9 100       35 ref $self || $self;
87             }
88              
89             sub asset_dir {
90 2     2 1 5 my $self = shift;
91              
92 2         3 my $asset_dir;
93 2 50 66     32 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         14 $asset_dir = $self->share_dir;
99             }
100             else {
101 0         0 $asset_dir = File::Spec->catfile($self->base_dir, 'share');
102             }
103 2         29 $self->_cache_method($asset_dir);
104             }
105              
106             sub base_dir {
107 3     3 0 6 my $self = shift;
108 3         13 my $class = $self->app_name;
109              
110 3         4 my $base_dir = do {
111 3         5 my $path = $class;
112 3         7 $path =~ s!::!/!g;
113 3   100     17 my $app_name = ref $self && $self->{app_name};
114 3 100 66     21 if (!$app_name and my $libpath = $INC{"$path.pm"}) {
115 2         4 $libpath =~ s!\\!/!g; # win32
116 2 50       36 if ($libpath =~ s!(?:blib/)?lib/+$path\.pm$!!) {
117 0   0     0 File::Spec->rel2abs($libpath || './');
118             }
119             else {
120 2         44 File::Spec->rel2abs('./');
121             }
122             }
123             else {
124 1         26 File::Spec->rel2abs('./');
125             }
126             };
127 3         16 $class->_cache_method($base_dir);
128             }
129              
130 0     0 0 0 sub mode_name { $ENV{PLACK_ENV} }
131 1     1 0 6 sub debug_mode { $ENV{PUNCHEUR_DEBUG} }
132              
133             # you can override 2 methods below
134 1     1 0 10 sub html_content_type { 'text/html; charset=UTF-8' }
135 2     2 0 10 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       7 my $tmpl = $self->{template_dir} ? $self->{template_dir} : File::Spec->catfile($self->asset_dir, 'tmpl');
145 1 50       7 my @tmpl = ref $tmpl ? @$tmpl : ($tmpl);
146              
147 2 0 33     143 @tmpl = map {
    50 0        
148 1         2 ref $_ && ref $_ eq 'CODE' ? $_->() :
149             ref $_ || File::Spec->file_name_is_absolute($_) ? $_ :
150             File::Spec->catfile($self->base_dir, $_)
151             } @tmpl;
152              
153 1         4 $self->_cache_method(\@tmpl);
154             }
155              
156             sub create_view {
157 1     1 0 1 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       6 } ) : () ),
191             },
192             },
193             };
194              
195 1         3 my @args;
196 1 50       3 if (my $v = $self->{view}) {
197 0 0       0 @args = !ref $v ? %{ $settings->{$v} } : %$v;
  0         0  
198             }
199             else {
200 1         1 @args = %{ $settings->{Xslate} };
  1         4  
201             }
202              
203 1         447 require Tiffany;
204 1         266 my $view = Tiffany->load(@args);
205             }
206              
207             sub view {
208 1     1 1 1 my $self = shift;
209              
210 1         5 $self->_cache_method($self->create_view);
211             }
212              
213             sub render {
214 1     1 0 2 my $self = shift;
215 1         5 my $html = $self->view->render(@_);
216              
217 1         419 for my $code ($self->get_trigger_code('HTML_FILTER')) {
218 0         0 $html = $code->($self, $html);
219             }
220              
221 1         12 $html = Encode::encode($self->encoding, $html);
222 1         183 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   19 no strict 'refs'; @{"$dispatcher_pkg\::ISA"} = ($base_dispatcher);
  3         4  
  3         763  
  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 6 my $self = shift;
272              
273 2   33     59 my $config_file = $self->{config} || File::Spec->catfile('config', 'common.pl');
274 2 50       7 return $config_file if ref $config_file;
275 2 50       26 $config_file = File::Spec->catfile($self->base_dir, $config_file)
276             unless File::Spec->file_name_is_absolute($config_file);
277              
278 2 50       35 -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         13 $self->_cache_method($self->load_config);
284             }
285              
286             # -------------------------------------------------------------------------
287             # Util:
288             sub add_method {
289 8     8 0 16 my ($klass, $method, $code) = @_;
290 3     3   47 no strict 'refs';
  3         6  
  3         3145  
291 8         11 *{"${klass}::${method}"} = $code;
  8         47  
292             }
293              
294             sub _cache_method {
295 9     9   11773 my ($self, $stuff) = @_;
296 9 100       52 return $stuff unless ref $self; # don't cache in class method
297              
298 5         34 my $class = $self->app_name;
299              
300 5         36 my (undef, undef, undef, $sub) = caller(1);
301 5         25 $sub = +(split /::/, $sub)[-1];
302 5     2   22 my $code = sub { $stuff };
  2         35  
303 5         31 $class->add_method($sub, $code);
304 5         16 $stuff;
305             }
306              
307             # -------------------------------------------------------------------------
308             # Attributes:
309 5     5 0 29 sub request { $_[0]->{request} }
310 1     1 0 11 sub req { $_[0]->{request} }
311              
312             sub session {
313 4     4 0 4 my $c = shift;
314 4   66     21 $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 _build_query {
352 0     0   0 my ($self, $query) = @_;
353              
354 0 0       0 my @query = !$query ? () : ref $query eq 'HASH' ? %$query : @$query;
    0          
355 0         0 my @q;
356 0         0 while (my ($key, $val) = splice @query, 0, 2) {
357 0         0 $val = URL::Encode::url_encode(Encode::encode($self->encoding, $val));
358 0         0 push @q, "${key}=${val}";
359             }
360 0 0       0 @q ? '?' . join('&', @q) : '';
361             }
362              
363             sub _build_uri {
364 0     0   0 my ($self, $root, $path, $query) = @_;
365              
366 0         0 $root =~ s{([^/])$}{$1/};
367 0         0 $path =~ s{^/}{};
368              
369 0         0 $root . $path . $self->_build_query($query);
370             }
371              
372             sub uri_for {
373 0     0 0 0 my ($self, $path, $query) = @_;
374 0   0     0 my $root = $self->req->{env}->{SCRIPT_NAME} || '/';
375              
376 0         0 $self->_build_uri($root, $path, $query);
377             }
378              
379             sub abs_uri_for {
380 0     0 0 0 my ($self, $path, $query) = @_;
381 0         0 my $root = $self->req->base;
382              
383 0         0 $self->_build_uri($root, $path, $query);
384             }
385              
386             # -------------------------------------------------------------------------
387             # PSGInise:
388             sub to_psgi {
389 1     1 0 2 my ($self, ) = @_;
390              
391 1 50       5 $self = $self->new unless ref $self;
392 1     2   9 return sub { $self->handle_request(shift) };
  2         12  
393             }
394 0     0 0 0 sub to_app { goto \&to_psgi }
395              
396             sub run {
397 0     0 0 0 my $self = shift;
398 0 0       0 my %opts = @_ == 1 ? %{$_[0]} : @_;
  0         0  
399              
400 0         0 my %server;
401 0         0 my $server = delete $opts{server};
402 0 0       0 $server{server} = $server if $server;
403              
404 0         0 my @options = %opts;
405 0         0 require Plack::Runner;
406              
407 0         0 my $runner = Plack::Runner->new(
408             %server,
409             options => \@options,
410             );
411 0         0 $runner->run($self->to_app);
412             }
413              
414             sub handle_request {
415 2     2 0 3 my ($self, $env) = @_;
416              
417 2         37 my $c = $self->clone;
418 2         10 $c->{request} = $c->create_request($env);
419              
420 2         23 local $_CONTEXT = $c;
421              
422 2         3 my $response;
423 2         11 for my $code ($c->get_trigger_code('BEFORE_DISPATCH')) {
424 0         0 $response = $code->($c);
425 0 0 0     0 goto PROCESS_END if Scalar::Util::blessed($response) && $response->isa('Plack::Response');
426             }
427 2 50       9 $response = $c->dispatch or die "cannot get any response";
428 2         13 PROCESS_END:
429             $c->call_trigger('AFTER_DISPATCH' => $response);
430              
431 2         8 return $response->finalize;
432             }
433              
434             # -------------------------------------------------------------------------
435             # Plugin
436             sub load_plugins {
437 1     1 0 3 my ($class, @args) = @_;
438 1         4 while (@args) {
439 2         3 my $module = shift @args;
440 2 50 66     12 my $conf = @args > 0 && ref $args[0] ? shift @args : undef;
441 2         4 $class->load_plugin($module, $conf);
442             }
443             }
444              
445             sub load_plugin {
446 3     3 0 5 my ($class, $module, $conf) = @_;
447              
448 3         16 $module = Plack::Util::load_class($module, 'Puncheur::Plugin');
449             {
450 3     3   14 no strict 'refs';
  3         13  
  3         118  
  3         16  
451 3         4 for my $method ( @{"${module}::EXPORT"} ){
  3         13  
452 3     3   12 use strict 'refs';
  3         3  
  3         548  
453 3         47 $class->add_method($method, $module->can($method));
454             }
455             }
456 3 50       28 $module->init($class, $conf) if $module->can('init');
457             }
458              
459             # -------------------------------------------------------------------------
460             # Raise Error:
461             my %StatusCode = (
462             400 => 'Bad Request',
463             401 => 'Unauthorized',
464             402 => 'Payment Required',
465             403 => 'Forbidden',
466             404 => 'Not Found',
467             405 => 'Method Not Allowed',
468             406 => 'Not Acceptable',
469             407 => 'Proxy Authentication Required',
470             408 => 'Request Timeout',
471             409 => 'Conflict',
472             410 => 'Gone',
473             411 => 'Length Required',
474             412 => 'Precondition Failed',
475             413 => 'Request Entity Too Large',
476             414 => 'Request-URI Too Large',
477             415 => 'Unsupported Media Type',
478             416 => 'Request Range Not Satisfiable',
479             417 => 'Expectation Failed',
480             418 => 'I\'m a teapot', # RFC 2324
481             422 => 'Unprocessable Entity', # RFC 2518 (WebDAV)
482             423 => 'Locked', # RFC 2518 (WebDAV)
483             424 => 'Failed Dependency', # RFC 2518 (WebDAV)
484             425 => 'No code', # WebDAV Advanced Collections
485             426 => 'Upgrade Required', # RFC 2817
486             428 => 'Precondition Required',
487             429 => 'Too Many Requests',
488             431 => 'Request Header Fields Too Large',
489             449 => 'Retry with', # unofficial Microsoft
490             500 => 'Internal Server Error',
491             501 => 'Not Implemented',
492             502 => 'Bad Gateway',
493             503 => 'Service Unavailable',
494             504 => 'Gateway Timeout',
495             505 => 'HTTP Version Not Supported',
496             506 => 'Variant Also Negotiates', # RFC 2295
497             507 => 'Insufficient Storage', # RFC 2518 (WebDAV)
498             509 => 'Bandwidth Limit Exceeded', # unofficial
499             510 => 'Not Extended', # RFC 2774
500             511 => 'Network Authentication Required',
501             );
502              
503             while ( my ($code, $msg) = each %StatusCode) {
504 3     3   13 no strict 'refs';
  3         5  
  3         116  
505             *{__PACKAGE__ ."::res_$code"} = sub {
506 3     3   12 use strict 'refs';
  3         4  
  3         337  
507 0     0     my $self = shift;
508 0           my $content = $self->error_html($code, $msg);
509 0           $self->create_response(
510             $code,
511             [
512             'Content-Type' => 'text/html; charset=utf-8',
513             'Content-Length' => length($content),
514             ],
515             [$content]
516             );
517             }
518             }
519              
520             # You can override it
521             sub error_html {
522 0     0 0   my ($self, $code, $msg) = @_;
523 0           sprintf q[
524            
525            
526            
527            
528            
529            
%s
530            
%s
531            
532             ], $code, $msg;
533             }
534              
535             1;
536             __END__