File Coverage

blib/lib/Kossy.pm
Criterion Covered Total %
statement 162 190 85.2
branch 35 56 62.5
condition 13 22 59.0
subroutine 32 35 91.4
pod 6 7 85.7
total 248 310 80.0


line stmt bran cond sub pod time code
1             package Kossy;
2              
3 10     10   607570 use strict;
  10         94  
  10         290  
4 10     10   50 use warnings;
  10         18  
  10         240  
5 10     10   285 use 5.008004;
  10         31  
6 10     10   5188 use utf8;
  10         130  
  10         49  
7 10     10   304 use Carp qw//;
  10         19  
  10         152  
8 10     10   46 use Cwd qw//;
  10         16  
  10         138  
9 10     10   42 use File::Basename qw//;
  10         25  
  10         154  
10 10     10   5300 use Text::Xslate;
  10         125920  
  10         584  
11 10     10   5787 use HTML::FillInForm::Lite qw//;
  10         36331  
  10         289  
12 10     10   6560 use JSON qw//;
  10         109240  
  10         296  
13 10     10   74 use Scalar::Util qw//;
  10         19  
  10         166  
14 10     10   4758 use Try::Tiny;
  10         18891  
  10         556  
15 10     10   4829 use Encode;
  10         142689  
  10         843  
16 10     10   4221 use Router::Boom;
  10         39139  
  10         450  
17             use Class::Accessor::Lite (
18 10         49 new => 0,
19             rw => [qw/root_dir/]
20 10     10   80 );
  10         20  
21 10     10   719 use base qw/Exporter/;
  10         38  
  10         1006  
22 10     10   4670 use Kossy::Exception;
  10         30  
  10         386  
23 10     10   4406 use Kossy::Connection;
  10         28  
  10         319  
24 10     10   4000 use Kossy::Request;
  10         34  
  10         343  
25 10     10   68 use Kossy::Response;
  10         20  
  10         188  
26 10     10   58 use HTTP::Headers::Fast;
  10         22  
  10         19540  
27              
28             our $VERSION = '0.60';
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 6     6 1 1421 my $class = shift;
41 6         11 my %args;
42 6 100       20 if ( @_ < 2 ) {
43 2         3 my $root_dir = shift;
44 2         7 my @caller = caller;
45 2   66     61 $root_dir ||= File::Basename::dirname( Cwd::realpath($caller[1]) );
46 2         6 $args{root_dir} = $root_dir;
47             }
48             else {
49 4         14 %args = @_;
50             }
51              
52 6         24 bless \%args, $class;
53             }
54              
55             sub psgi {
56 3     3 1 1947 my $self = shift;
57 3 50       15 if ( ! ref $self ) {
58 3         5 my %args;
59 3 50       12 if ( @_ < 2 ) {
60 3         7 my $root_dir = shift;
61 3         11 my @caller = caller;
62 3   66     128 $root_dir ||= File::Basename::dirname( Cwd::realpath($caller[1]) );
63 3         12 $args{root_dir} = $root_dir;
64             }
65             else {
66 0         0 %args = @_;
67             }
68 3         16 $self = $self->new(%args);
69             }
70              
71 3         14 $self->build_app;
72             }
73              
74             sub build_app {
75 3     3 0 7 my $self = shift;
76              
77             #router
78 3         27 my $router = Router::Boom->new;
79 3         99 $router->add($_ => $self->_router->{$_} ) for keys %{$self->_router};
  3         11  
80 3         107 my $security_header_local = $SECURITY_HEADER;
81 3         6 my %match_cache;
82              
83 3         21 my $tx = __PACKAGE__->_build_text_xslate(
84             root_dir => $self->root_dir,
85             cache => $XSLATE_CACHE,
86             cache_dir => $XSLATE_CACHE_DIR,
87             );
88              
89 3         24 my $json_serializer = __PACKAGE__->_build_json_serializer();
90              
91             sub {
92 19     19   103788 my $env = shift;
93 19         47 $Kossy::Response::SECURITY_HEADER = $security_header_local;
94             try {
95 19 100       837 my $header = bless {
96             'content-type' => 'text/html; charset=UTF-8',
97             $security_header_local ? ('x-frame-options' => 'DENY') : (),
98             }, 'HTTP::Headers::Fast';
99 19         156 my $c = Kossy::Connection->new({
100             tx => $tx,
101             req => Kossy::Request->new($env),
102             res => Kossy::Response->new(200, $header),
103             stash => {},
104             json_serializer => $json_serializer,
105             });
106 19         319 my $method = uc($env->{REQUEST_METHOD});
107 19         68 my $cache_key = $method . '-' . $env->{PATH_INFO};
108             my ($match,$args) = try {
109 19 100       785 if ( exists $match_cache{$cache_key} ) {
110 2         6 return @{$match_cache{$cache_key}};
  2         8  
111             }
112 17         303 my $path_info = Encode::decode_utf8( $env->{PATH_INFO}, Encode::FB_CROAK | Encode::LEAVE_SRC );
113 17         230 my @match = $router->match($path_info);
114 17 100       6105 if ( !@match ) {
115 2         11 $c->halt(404);
116             }
117            
118 15 100       65 if ( !exists $match[0]->{$method}) {
119 2         11 $c->halt(405);
120             }
121             $match_cache{$cache_key} = [$match[0]->{$method},$match[1]]
122 13 100       21 if ! scalar keys %{$match[1]};
  13         75  
123 13         55 return ($match[0]->{$method},$match[1]);
124             } catch {
125 4 50 33     85 if ( ref $_ && ref $_ eq 'Kossy::Exception' ) {
126 4         21 die $_; #rethrow
127             }
128 0         0 $c->halt(400,'unexpected character in request');
129 19         168 };
130            
131 15         413 my $code = $match->{__action__};
132 15   50     55 my $filters = $match->{__filter__} || [];
133 15 50 33     99 if ( $] == 5.020000 || $] == 5.020100 ) {
134             # workaround for 5.20.0 or 5.20.1 https://github.com/kazeburo/Kossy/pull/10
135 0         0 my %args = map { $_ => Encode::decode_utf8($args->{$_}) } keys %$args;
  0         0  
136 0         0 $c->args(\%args);
137             } else {
138 15         93 $c->args({%$args});
139             }
140             my $app = sub {
141 15         39 my ($self, $c) = @_;
142 15         29 my $response;
143 15         67 my $res = $code->($self, $c);
144 15 50       600 Carp::croak "Undefined Response" if ! defined $res;
145 15   100     70 my $res_t = ref($res) || '';
146 15 100       90 if ( $res_t eq 'Kossy::Response' ) {
    50          
    50          
    50          
147 2         7 $response = $res;
148             }
149             elsif ( $res_t eq 'Plack::Response' ) {
150 0         0 $response = bless $res, 'Kossy::Response';
151             }
152             elsif ( $res_t eq 'ARRAY' ) {
153 0         0 $response = Kossy::Response->new(@$res);
154             }
155             elsif ( !$res_t ) {
156 13         35 $c->res->body($res);
157 13         105 $response = $c->res;
158             }
159             else {
160 0         0 Carp::croak sprintf "Unknown Response: %s", $res_t;
161             }
162 15         101 $response;
163 15         176 };
164            
165 15         53 for my $filter ( reverse @$filters ) {
166 0         0 $app = $self->_wrap_filter($filter,$app);
167             }
168             # do all
169 15         28 local $Kossy::Response::DIRECT;
170 15         34 $app->($self, $c)->finalize;
171             } catch {
172 4 50 33     115 if ( ref $_ && ref $_ eq 'Kossy::Exception' ) {
173 4         18 return $_->response;
174             }
175 0         0 die $_;
176 19         199 };
177 3         28 };
178             }
179              
180             sub _build_text_xslate {
181 5     5   7634 my ($class, %args) = @_;
182 5         23 my ($root_dir, $cache, $cache_dir) = @args{qw/root_dir cache cache_dir/};
183              
184 5         65 my $fif = HTML::FillInForm::Lite->new();
185             my $tx = Text::Xslate->new(
186             path => [ $root_dir . '/views' ],
187             input_layer => ':utf8',
188             module => ['Text::Xslate::Bridge::TT2Like','Number::Format' => [':subs']],
189             function => {
190             fillinform => sub {
191 0     0   0 my $q = shift;
192             return sub {
193 0         0 my ($html) = @_;
194 0         0 return Text::Xslate::mark_raw( $fif->fill( \$html, $q ) );
195             }
196 0         0 }
197             },
198 5 100       277 cache => $cache,
199             defined $cache_dir ? ( cache_dir => $cache_dir ) : (),
200             );
201 5         127347 return $tx
202             }
203              
204             sub _build_json_serializer {
205 7     7   11200 my $class = shift;
206              
207 7 100       27 if (defined $JSON_SERIALIZER) {
208 4 100 100     57 if (Scalar::Util::blessed($JSON_SERIALIZER) && $JSON_SERIALIZER->can('encode')) {
209 2         8 return $JSON_SERIALIZER
210             }
211             else {
212 2         312 Carp::croak '$Kossy::JSON_SERIALIZER must have `encode` method';
213             }
214             }
215              
216             # default case
217 3         101 return JSON->new()->allow_blessed(1)->convert_blessed(1)->ascii(1);
218             }
219              
220              
221             my $_ROUTER={};
222             sub _router {
223 75     75   900 my $klass = shift;
224 75 100       121 my $class = ref $klass ? ref $klass : $klass;
225 75 100       152 if ( !$_ROUTER->{$class} ) {
226 4         10 $_ROUTER->{$class} = {};
227             }
228 75         229 $_ROUTER->{$class};
229             }
230              
231             sub _connect {
232 28     28   43 my $class = shift;
233 28         49 my ( $methods, $pattern, $filter, $code ) = @_;
234 28 50       59 $methods = ref($methods) ? $methods : [$methods];
235 28 50       50 if (!$code) {
236 28         36 $code = $filter;
237 28         38 $filter = [];
238             }
239 28         45 for my $method ( @$methods ) {
240 53         145 $class->_router->{$pattern}->{$method} = {
241             __action__ => $code,
242             __filter__ => $filter
243             };
244             }
245             }
246              
247             sub get {
248 22     22 1 2279 my $class = caller;
249 22         64 $class->_connect( ['GET','HEAD'], @_ );
250             }
251              
252             sub post {
253 3     3 1 17 my $class = caller;
254 3         25 $class->_connect( ['POST'], @_ );
255             }
256              
257             sub router {
258 3     3 1 26 my $class = caller;
259 3         12 $class->_connect( @_ );
260             }
261              
262             my $_FILTER={};
263             sub filter {
264 0     0 1   my $class = caller;
265 0 0         if ( !$_FILTER->{$class} ) {
266 0           $_FILTER->{$class} = {};
267             }
268 0 0         if ( @_ ) {
269 0           $_FILTER->{$class}->{$_[0]} = $_[1];
270             }
271 0           $_FILTER->{$class};
272             }
273              
274             sub _wrap_filter {
275 0     0     my $klass = shift;
276 0 0         my $class = ref $klass ? ref $klass : $klass;
277 0 0         if ( !$_FILTER->{$class} ) {
278 0           $_FILTER->{$class} = {};
279             }
280 0           my ($filter,$app) = @_;
281 0           my $filter_subref = $_FILTER->{$class}->{$filter};
282 0 0         Carp::croak sprintf("Filter:%s is not exists", $filter) unless $filter_subref;
283 0           return $filter_subref->($app);
284             }
285              
286             1;
287              
288             __END__