File Coverage

blib/lib/Kossy.pm
Criterion Covered Total %
statement 173 187 92.5
branch 42 56 75.0
condition 15 24 62.5
subroutine 34 35 97.1
pod 6 7 85.7
total 270 309 87.3


line stmt bran cond sub pod time code
1             package Kossy;
2              
3 15     15   2327191 use strict;
  15         30  
  15         530  
4 15     15   63 use warnings;
  15         30  
  15         991  
5 15     15   299 use 5.014004;
  15         52  
6 15     15   5920 use utf8;
  15         3263  
  15         118  
7 15     15   563 use Carp qw//;
  15         22  
  15         301  
8 15     15   61 use Cwd qw//;
  15         19  
  15         237  
9 15     15   65 use File::Basename qw//;
  15         19  
  15         253  
10 15     15   9092 use Text::Xslate;
  15         251346  
  15         1229  
11 15     15   10324 use HTML::FillInForm::Lite qw//;
  15         69018  
  15         665  
12 15     15   11221 use JSON qw//;
  15         246669  
  15         659  
13 15     15   197 use Scalar::Util qw//;
  15         30  
  15         373  
14 15     15   7828 use Try::Tiny;
  15         34246  
  15         1052  
15 15     15   6688 use Encode;
  15         257232  
  15         1821  
16 15     15   9409 use Router::Boom::Method;
  15         102268  
  15         916  
17             use Class::Accessor::Lite (
18 15         84 new => 0,
19             rw => [qw/root_dir/]
20 15     15   235 );
  15         35  
21 15     15   1262 use base qw/Exporter/;
  15         73  
  15         1591  
22 15     15   8394 use Kossy::Exception;
  15         66  
  15         685  
23 15     15   8366 use Kossy::Connection;
  15         46  
  15         622  
24 15     15   7572 use Kossy::Request;
  15         62  
  15         622  
25 15     15   95 use Kossy::Response;
  15         44  
  15         289  
26 15     15   58 use HTTP::Headers::Fast;
  15         26  
  15         34232  
27              
28             our $VERSION = '0.63';
29             our @EXPORT = qw/new root_dir psgi build_app _router _connect get post router filter _wrap_filter/;
30              
31             our $XSLATE_CACHE = 1;
32             our $XSLATE_CACHE_DIR;
33             our $SECURITY_HEADER = 1;
34             our $JSON_SERIALIZER;
35              
36             # cache underscore translation
37             HTTP::Headers::Fast::_standardize_field_name('X-Frame-Options');
38              
39             sub new {
40 9     9 1 274433 my $class = shift;
41 9         18 my %args;
42 9 100       34 if ( @_ < 2 ) {
43 2         4 my $root_dir = shift;
44 2         8 my @caller = caller;
45 2   66     88 $root_dir ||= File::Basename::dirname( Cwd::realpath($caller[1]) );
46 2         10 $args{root_dir} = $root_dir;
47             }
48             else {
49 7         29 %args = @_;
50             }
51              
52 9         41 bless \%args, $class;
53             }
54              
55             sub psgi {
56 6     6 1 351855 my $self = shift;
57 6 50       28 if ( ! ref $self ) {
58 6         13 my %args;
59 6 50       24 if ( @_ < 2 ) {
60 6         38 my $root_dir = shift;
61 6         26 my @caller = caller;
62 6   66     675 $root_dir ||= File::Basename::dirname( Cwd::realpath($caller[1]) );
63 6         30 $args{root_dir} = $root_dir;
64             }
65             else {
66 0         0 %args = @_;
67             }
68 6         60 $self = $self->new(%args);
69             }
70              
71 6         41 $self->build_app;
72             }
73              
74             sub build_app {
75 6     6 0 19 my $self = shift;
76              
77 6         14 my $security_header_local = $SECURITY_HEADER;
78 6         13 my %match_cache;
79              
80 6         57 my $tx = __PACKAGE__->_build_text_xslate(
81             root_dir => $self->root_dir,
82             cache => $XSLATE_CACHE,
83             cache_dir => $XSLATE_CACHE_DIR,
84             );
85              
86 6         56 my $json_serializer = __PACKAGE__->_build_json_serializer();
87              
88             sub {
89 35     35   215303 my $env = shift;
90 35         89 $Kossy::Response::SECURITY_HEADER = $security_header_local;
91             try {
92 35 100       1494 my $header = bless {
93             'content-type' => 'text/html; charset=UTF-8',
94             $security_header_local ? ('x-frame-options' => 'DENY') : (),
95             }, 'HTTP::Headers::Fast';
96 35         241 my $c = Kossy::Connection->new({
97             tx => $tx,
98             req => Kossy::Request->new($env),
99             res => Kossy::Response->new(200, $header),
100             stash => {},
101             json_serializer => $json_serializer,
102             });
103 35         515 my $method = uc($env->{REQUEST_METHOD});
104 35         94 my $cache_key = $method . '-' . $env->{PATH_INFO};
105             my ($match,$args) = try {
106 35 100       1395 if ( exists $match_cache{$cache_key} ) {
107 2         2 return @{$match_cache{$cache_key}};
  2         6  
108             }
109 33         444 my $path_info = Encode::decode_utf8( $env->{PATH_INFO}, Encode::FB_CROAK | Encode::LEAVE_SRC );
110 33         281 my ($dest, $captured, $method_not_allowed) = $self->_router->match($method, $path_info);
111 33 100       15873 if ($method_not_allowed) {
112 3         11 $c->halt(405);
113             }
114              
115 30 100       101 if (!$dest) {
116 4         17 $c->halt(404);
117             }
118              
119 26         109 $match_cache{$cache_key} = [$dest, $captured];
120 26         86 return ($dest, $captured);
121             } catch {
122 7 50 33     135 if ( ref $_ && ref $_ eq 'Kossy::Exception' ) {
123 7         117 die $_; #rethrow
124             }
125 0         0 $c->halt(400,'unexpected character in request');
126 35         276 };
127              
128 28         661 my $code = $match->{__action__};
129 28   50     92 my $filters = $match->{__filter__} || [];
130 28 50 33     139 if ( $] == 5.020000 || $] == 5.020100 ) {
131             # workaround for 5.20.0 or 5.20.1 https://github.com/kazeburo/Kossy/pull/10
132 0         0 my %args = map { $_ => Encode::decode_utf8($args->{$_}) } keys %$args;
  0         0  
133 0         0 $c->args(\%args);
134             } else {
135 28         150 $c->args({%$args});
136             }
137             my $app = sub {
138 28         77 my ($self, $c) = @_;
139 28         34 my $response;
140 28         102 my $res = $code->($self, $c);
141 15 50       485 Carp::croak "Undefined Response" if ! defined $res;
142 15   100     76 my $res_t = ref($res) || '';
143 15 100       55 if ( $res_t eq 'Kossy::Response' ) {
    50          
    50          
    50          
144 2         3 $response = $res;
145             }
146             elsif ( $res_t eq 'Plack::Response' ) {
147 0         0 $response = bless $res, 'Kossy::Response';
148             }
149             elsif ( $res_t eq 'ARRAY' ) {
150 0         0 $response = Kossy::Response->new(@$res);
151             }
152             elsif ( !$res_t ) {
153 13         30 $c->res->body($res);
154 13         85 $response = $c->res;
155             }
156             else {
157 0         0 Carp::croak sprintf "Unknown Response: %s", $res_t;
158             }
159 15         73 $response;
160 28         283 };
161              
162 28         67 for my $filter ( reverse @$filters ) {
163 1         5 $app = $self->_wrap_filter($filter,$app);
164             }
165             # do all
166 28         48 local $Kossy::Response::DIRECT;
167 28         54 $app->($self, $c)->finalize;
168             } catch {
169 20 50 33     361 if ( ref $_ && ref $_ eq 'Kossy::Exception' ) {
170 20         94 return $_->response;
171             }
172 0         0 die $_;
173 35         361 };
174 6         88 };
175             }
176              
177             sub _build_text_xslate {
178 8     8   174520 my ($class, %args) = @_;
179 8         35 my ($root_dir, $cache, $cache_dir) = @args{qw/root_dir cache cache_dir/};
180              
181 8         96 my $fif = HTML::FillInForm::Lite->new();
182             my $tx = Text::Xslate->new(
183             path => [ $root_dir . '/views' ],
184             input_layer => ':utf8',
185             module => ['Text::Xslate::Bridge::TT2Like','Number::Format' => [':subs']],
186             function => {
187             fillinform => sub {
188 0     0   0 my $q = shift;
189             return sub {
190 0         0 my ($html) = @_;
191 0         0 return Text::Xslate::mark_raw( $fif->fill( \$html, $q ) );
192             }
193 0         0 }
194             },
195 8 100       477 cache => $cache,
196             defined $cache_dir ? ( cache_dir => $cache_dir ) : (),
197             );
198 8         246499 return $tx
199             }
200              
201             sub _build_json_serializer {
202 10     10   280248 my $class = shift;
203              
204 10 100       46 if (defined $JSON_SERIALIZER) {
205 4 100 100     74 if (Scalar::Util::blessed($JSON_SERIALIZER) && $JSON_SERIALIZER->can('encode')) {
206 2         8 return $JSON_SERIALIZER
207             }
208             else {
209 2         357 Carp::croak '$Kossy::JSON_SERIALIZER must have `encode` method';
210             }
211             }
212              
213             # default case
214 6         341 return JSON->new()->allow_blessed(1)->convert_blessed(1)->ascii(1);
215             }
216              
217              
218             my $_ROUTER={};
219             sub _router {
220 75     75   127 my $klass = shift;
221 75 100       155 my $class = ref $klass ? ref $klass : $klass;
222 75 100       209 if ( !$_ROUTER->{$class} ) {
223 7         72 $_ROUTER->{$class} = Router::Boom::Method->new;
224             }
225 75         368 $_ROUTER->{$class};
226             }
227              
228             sub _connect {
229 43     43   61 my $class = shift;
230 43         120 my ( $methods, $pattern, $filter, $code ) = @_;
231 43 100       113 $methods = ref($methods) ? $methods : [$methods];
232 43 100       100 if (!$code) {
233 42         65 $code = $filter;
234 42         62 $filter = [];
235             }
236 43 100 100     122 unless ( (ref $code ||'') eq 'CODE') {
237 1         222 Carp::croak "\$code argument must be a CODE reference";
238             }
239 42         99 $class->_router->add($methods, $pattern, {
240             __action__ => $code,
241             __filter__ => $filter
242             });
243             }
244              
245             sub get {
246 34     34 1 814156 my $class = caller;
247 34         147 $class->_connect( ['GET','HEAD'], @_ );
248             }
249              
250             sub post {
251 4     4 1 94 my $class = caller;
252 4         12 $class->_connect( ['POST'], @_ );
253             }
254              
255             sub router {
256 5     5 1 92 my $class = caller;
257 5         14 $class->_connect( @_ );
258             }
259              
260             my $_FILTER={};
261             sub filter {
262 1     1 1 3069 my $class = caller;
263 1 50       6 if ( !$_FILTER->{$class} ) {
264 1         3 $_FILTER->{$class} = {};
265             }
266 1 50       4 if ( @_ ) {
267 1         5 $_FILTER->{$class}->{$_[0]} = $_[1];
268             }
269 1         3 $_FILTER->{$class};
270             }
271              
272             sub _wrap_filter {
273 1     1   3 my $klass = shift;
274 1 50       5 my $class = ref $klass ? ref $klass : $klass;
275 1 50       4 if ( !$_FILTER->{$class} ) {
276 0         0 $_FILTER->{$class} = {};
277             }
278 1         3 my ($filter,$app) = @_;
279 1         3 my $filter_subref = $_FILTER->{$class}->{$filter};
280 1 50       7 Carp::croak sprintf("Filter:%s is not exists", $filter) unless $filter_subref;
281 1         4 return $filter_subref->($app);
282             }
283              
284             1;
285              
286             __END__