File Coverage

blib/lib/Raisin.pm
Criterion Covered Total %
statement 140 166 84.3
branch 32 48 66.6
condition 3 12 25.0
subroutine 33 35 94.2
pod 5 19 26.3
total 213 280 76.0


line stmt bran cond sub pod time code
1             #!perl
2             #PODNAME: Raisin
3             #ABSTRACT: A REST API microframework for Perl.
4              
5 12     12   824981 use strict;
  12         63  
  12         446  
6 12     12   70 use warnings;
  12         29  
  12         690  
7              
8             package Raisin;
9             $Raisin::VERSION = '0.92';
10 12     12   69 use Carp qw(croak carp longmess);
  12         22  
  12         826  
11 12     12   4371 use HTTP::Status qw(:constants);
  12         39215  
  12         5144  
12 12     12   6207 use Plack::Response;
  12         129550  
  12         418  
13 12     12   4851 use Plack::Util;
  12         93897  
  12         396  
14              
15 12     12   5730 use Raisin::Request;
  12         43  
  12         464  
16 12     12   6291 use Raisin::Routes;
  12         35  
  12         394  
17 12     12   81 use Raisin::Util;
  12         30  
  12         229  
18              
19 12     12   5601 use Raisin::Middleware::Formatter;
  12         32  
  12         383  
20 12     12   5600 use Raisin::Encoder;
  12         31  
  12         349  
21 12     12   4807 use Raisin::Decoder;
  12         29  
  12         393  
22              
23 12         76 use Plack::Util::Accessor qw(
24             middleware
25             mounted
26             routes
27              
28             decoder
29             encoder
30 12     12   76 );
  12         25  
31              
32             sub new {
33 9     9 0 11372 my ($class, %args) = @_;
34              
35 9         44 my $self = bless { %args }, $class;
36              
37 9         58 $self->middleware({});
38 9         130 $self->mounted([]);
39 9         112 $self->routes(Raisin::Routes->new);
40              
41 9         160 $self->decoder(Raisin::Decoder->new);
42 9         66 $self->encoder(Raisin::Encoder->new);
43              
44 9         778 $self;
45             }
46              
47             sub mount_package {
48 2     2 0 5 my ($self, $package) = @_;
49 2         4 push @{ $self->{mounted} }, $package;
  2         10  
50 2         6 Plack::Util::load_class($package);
51             }
52              
53             sub load_plugin {
54 10     10 0 38 my ($self, $name, @args) = @_;
55 10 100       47 return if $self->{loaded_plugins}{$name};
56              
57 8         66 my $class = Plack::Util::load_class($name, 'Raisin::Plugin');
58 8         136 my $module = $self->{loaded_plugins}{$name} = $class->new($self);
59              
60 8         37 $module->build(@args);
61             }
62              
63             sub add_middleware {
64 2     2 0 8 my ($self, $name, @args) = @_;
65 2         15 $self->{middleware}{$name} = \@args;
66             }
67              
68             # Routes
69             sub add_route {
70 51     51 0 250 my ($self, %params) = @_;
71 51         153 $self->routes->add(%params);
72             }
73              
74             # Resource description
75             sub resource_desc {
76 9     9 0 24 my ($self, $ns, $desc) = @_;
77 9 100       28 $self->{resource_desc}{$ns} = $desc if $desc;
78 9         29 $self->{resource_desc}{$ns};
79             }
80              
81             # Hooks
82             sub hook {
83 120     120 0 235 my ($self, $name) = @_;
84 120 100   116   770 $self->{hooks}{$name} || sub {};
85             }
86              
87             sub add_hook {
88 4     4 0 10 my ($self, $name, $code) = @_;
89 4         15 $self->{hooks}{$name} = $code;
90             }
91              
92             # Application
93             sub run {
94 7     7 1 18 my $self = shift;
95 7     30   33 my $psgi = sub { $self->psgi(@_) };
  30         514  
96              
97 7         34 $self->{allowed_methods} = $self->generate_allowed_methods;
98              
99             # Add middleware
100 7         20 for my $class (keys %{ $self->{middleware} }) {
  7         27  
101             # Make sure the middleware was not already loaded
102 2 50       14 next if $self->{_loaded_middleware}->{$class}++;
103              
104 2         11 my $mw = Plack::Util::load_class($class, 'Plack::Middleware');
105 2         3418 my $args = $self->{middleware}{$class};
106 2         31 $psgi = $mw->wrap($psgi, @$args);
107             }
108              
109 7         339 $psgi = Raisin::Middleware::Formatter->wrap(
110             $psgi,
111             default_format => $self->default_format,
112             format => $self->format,
113             decoder => $self->decoder,
114             encoder => $self->encoder,
115             raisin => $self,
116             );
117              
118             # load fallback logger (Raisin::Logger)
119 7         532 $self->load_plugin('Logger', fallback => 1);
120              
121 7         34 return $psgi;
122             }
123              
124             sub generate_allowed_methods {
125 7     7 0 14 my $self = shift;
126              
127 7         14 my %allowed_methods_by_endpoint;
128              
129             # `options` for each `path`
130 7         14 for my $path (keys %{ $self->routes->list }) {
  7         26  
131             my $methods = join ', ',
132 11         62 sort(keys(%{ $self->routes->list->{$path} }), 'OPTIONS');
  11         37  
133              
134             $self->add_route(
135             method => 'OPTIONS',
136             path => $path,
137             code => sub {
138 0     0   0 $self->res->headers([Allow => $methods]);
139 0         0 undef;
140             },
141 11         150 );
142              
143 11         88 $allowed_methods_by_endpoint{$path} = $methods;
144             }
145              
146 7         55 \%allowed_methods_by_endpoint;
147             }
148              
149             sub psgi {
150 30     30 0 79 my ($self, $env) = @_;
151              
152             # New for each response
153 30         197 my $req = $self->req(Raisin::Request->new($env));
154 30         187 my $res = $self->res(Plack::Response->new);
155              
156             # Generate API description
157 30 100       154 if ($self->can('swagger_build_spec')) {
158 17         81 $self->swagger_build_spec;
159             }
160              
161             my $ret = eval {
162 30         104 $self->hook('before')->($self);
163              
164             # Find a route
165 30         350 my $route = $self->routes->find($req->method, $req->path);
166             # The requested path exists but requested method not
167 30 50 33     139 if (!$route && $self->{allowed_methods}{ $req->path }) {
    50          
168 0         0 $res->status(HTTP_METHOD_NOT_ALLOWED);
169 0         0 return $res->finalize;
170             }
171             # Nothing found
172             elsif (!$route) {
173 0         0 $res->status(HTTP_NOT_FOUND);
174 0         0 return $res->finalize;
175             }
176              
177 30         89 my $code = $route->code;
178 30 50 33     339 if (!$code || ($code && ref($code) ne 'CODE')) {
      33        
179 0         0 $self->log(error => "route ${ \$req->path } returns nothing or not CODE");
  0         0  
180              
181 0         0 $res->status(HTTP_INTERNAL_SERVER_ERROR);
182 0         0 $res->body('Internal error');
183              
184 0         0 return $res->finalize;
185             }
186              
187 30         89 $self->hook('before_validation')->($self);
188              
189             # Validation and coercion of declared params
190 30 50       173 if (!$req->prepare_params($route->params, $route->named)) {
191 0         0 $res->status(HTTP_BAD_REQUEST);
192 0         0 $res->body('Invalid Parameters');
193 0         0 return $res->finalize;
194             }
195              
196 30         99 $self->hook('after_validation')->($self);
197              
198             # Evaluate an endpoint
199 30         230 my $data = $code->($req->declared_params);
200 30 50       157 if (defined $data) {
201             # Delayed response
202 30 50       107 return $data if ref($data) eq 'CODE';
203              
204 30         113 $res->body($data);
205             }
206              
207 30         215 $self->hook('after')->($self);
208              
209 30         206 1;
210 30 50       62 } or do {
211 0         0 my ($e) = longmess($@);
212 0         0 $self->log(error => $e);
213              
214             my $msg = $ENV{PLACK_ENV}
215 0 0 0     0 && $ENV{PLACK_ENV} eq 'deployment' ? 'Internal Error' : $e;
216              
217 0         0 $res->status(HTTP_INTERNAL_SERVER_ERROR);
218 0         0 $res->body($msg);
219             };
220              
221 30 50       89 if (ref($ret) eq 'CODE') {
222 0         0 return $ret;
223             }
224              
225 30         94 $self->finalize;
226             }
227              
228             # Finalize response
229             sub before_finalize {
230 30     30 0 56 my $self = shift;
231              
232 30 50       70 $self->res->status(HTTP_OK) unless $self->res->status;
233 30         169 $self->res->header('X-Framework' => 'Raisin ' . __PACKAGE__->VERSION);
234              
235 30 50       1713 if ($self->api_version) {
236 0         0 $self->res->header('X-API-Version' => $self->api_version);
237             }
238             }
239              
240             sub finalize {
241 30     30 0 56 my $self = shift;
242 30         80 $self->before_finalize;
243 30         70 $self->res->finalize;
244             }
245              
246             # Application defaults
247             sub default_format {
248 12     12 0 35 my ($self, $format) = @_;
249              
250 12 100       48 if ($format) {
251 3         9 $self->{default_format} = $format;
252             }
253              
254 12 100       102 $self->{default_format} || 'yaml';
255             }
256              
257             sub format {
258 12     12 0 38 my ($self, $format) = @_;
259              
260 12 100       40 if ($format) {
261 2         6 my @decoders = keys %{ $self->decoder->all };
  2         10  
262              
263 2 50       7 if (grep { lc($format) eq $_ } @decoders) {
  4         17  
264 2         7 $self->{format} = lc $format;
265 2         9 $self->default_format(lc $format);
266             }
267             else {
268 0         0 carp 'Invalid format, choose one of: ', join(', ', @decoders);
269             }
270             }
271              
272 12         57 $self->{format};
273             }
274              
275             sub api_version {
276 38     38 1 97 my ($self, $version) = @_;
277 38 100       88 $self->{version} = $version if $version;
278             $self->{version}
279 38         188 }
280              
281             # Request and Response and shortcuts
282             sub req {
283 36     36 1 356 my ($self, $req) = @_;
284 36 100       293 $self->{req} = $req if $req;
285 36         666 $self->{req};
286             }
287              
288             sub res {
289 167     167 1 793 my ($self, $res) = @_;
290 167 100       438 $self->{res} = $res if $res;
291 167         904 $self->{res};
292             }
293              
294             sub session {
295 0     0 1   my $self = shift;
296              
297 0 0         if (not $self->req->env->{'psgix.session'}) {
298 0           croak "No Session middleware wrapped";
299             }
300              
301 0           $self->req->session;
302             }
303              
304             1;
305              
306             __END__