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   791886 use strict;
  12         69  
  12         434  
6 12     12   68 use warnings;
  12         23  
  12         632  
7              
8             package Raisin;
9             $Raisin::VERSION = '0.93';
10 12     12   71 use Carp qw(croak carp longmess);
  12         21  
  12         734  
11 12     12   4419 use HTTP::Status qw(:constants);
  12         39688  
  12         5004  
12 12     12   6312 use Plack::Response;
  12         129277  
  12         408  
13 12     12   4753 use Plack::Util;
  12         92467  
  12         380  
14              
15 12     12   5468 use Raisin::Request;
  12         47  
  12         472  
16 12     12   6446 use Raisin::Routes;
  12         36  
  12         389  
17 12     12   79 use Raisin::Util;
  12         25  
  12         231  
18              
19 12     12   5525 use Raisin::Middleware::Formatter;
  12         32  
  12         384  
20 12     12   5370 use Raisin::Encoder;
  12         32  
  12         356  
21 12     12   4811 use Raisin::Decoder;
  12         28  
  12         409  
22              
23 12         77 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 11326 my ($class, %args) = @_;
34              
35 9         47 my $self = bless { %args }, $class;
36              
37 9         58 $self->middleware({});
38 9         136 $self->mounted([]);
39 9         107 $self->routes(Raisin::Routes->new);
40              
41 9         162 $self->decoder(Raisin::Decoder->new);
42 9         78 $self->encoder(Raisin::Encoder->new);
43              
44 9         816 $self;
45             }
46              
47             sub mount_package {
48 2     2 0 6 my ($self, $package) = @_;
49 2         4 push @{ $self->{mounted} }, $package;
  2         7  
50 2         7 Plack::Util::load_class($package);
51             }
52              
53             sub load_plugin {
54 10     10 0 34 my ($self, $name, @args) = @_;
55 10 100       47 return if $self->{loaded_plugins}{$name};
56              
57 8         64 my $class = Plack::Util::load_class($name, 'Raisin::Plugin');
58 8         208 my $module = $self->{loaded_plugins}{$name} = $class->new($self);
59              
60 8         38 $module->build(@args);
61             }
62              
63             sub add_middleware {
64 2     2 0 8 my ($self, $name, @args) = @_;
65 2         14 $self->{middleware}{$name} = \@args;
66             }
67              
68             # Routes
69             sub add_route {
70 51     51 0 301 my ($self, %params) = @_;
71 51         155 $self->routes->add(%params);
72             }
73              
74             # Resource description
75             sub resource_desc {
76 9     9 0 22 my ($self, $ns, $desc) = @_;
77 9 100       27 $self->{resource_desc}{$ns} = $desc if $desc;
78 9         25 $self->{resource_desc}{$ns};
79             }
80              
81             # Hooks
82             sub hook {
83 120     120 0 231 my ($self, $name) = @_;
84 120 100   116   834 $self->{hooks}{$name} || sub {};
85             }
86              
87             sub add_hook {
88 4     4 0 8 my ($self, $name, $code) = @_;
89 4         16 $self->{hooks}{$name} = $code;
90             }
91              
92             # Application
93             sub run {
94 7     7 1 17 my $self = shift;
95 7     30   31 my $psgi = sub { $self->psgi(@_) };
  30         492  
96              
97 7         27 $self->{allowed_methods} = $self->generate_allowed_methods;
98              
99             # Add middleware
100 7         17 for my $class (keys %{ $self->{middleware} }) {
  7         30  
101             # Make sure the middleware was not already loaded
102 2 50       12 next if $self->{_loaded_middleware}->{$class}++;
103              
104 2         12 my $mw = Plack::Util::load_class($class, 'Plack::Middleware');
105 2         3432 my $args = $self->{middleware}{$class};
106 2         27 $psgi = $mw->wrap($psgi, @$args);
107             }
108              
109 7         320 $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         536 $self->load_plugin('Logger', fallback => 1);
120              
121 7         33 return $psgi;
122             }
123              
124             sub generate_allowed_methods {
125 7     7 0 16 my $self = shift;
126              
127 7         14 my %allowed_methods_by_endpoint;
128              
129             # `options` for each `path`
130 7         15 for my $path (keys %{ $self->routes->list }) {
  7         25  
131             my $methods = join ', ',
132 11         62 sort(keys(%{ $self->routes->list->{$path} }), 'OPTIONS');
  11         28  
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         167 );
142              
143 11         101 $allowed_methods_by_endpoint{$path} = $methods;
144             }
145              
146 7         54 \%allowed_methods_by_endpoint;
147             }
148              
149             sub psgi {
150 30     30 0 68 my ($self, $env) = @_;
151              
152             # New for each response
153 30         195 my $req = $self->req(Raisin::Request->new($env));
154 30         201 my $res = $self->res(Plack::Response->new);
155              
156             # Generate API description
157 30 100       155 if ($self->can('swagger_build_spec')) {
158 17         54 $self->swagger_build_spec;
159             }
160              
161             my $ret = eval {
162 30         96 $self->hook('before')->($self);
163              
164             # Find a route
165 30         393 my $route = $self->routes->find($req->method, $req->path);
166             # The requested path exists but requested method not
167 30 50 33     149 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         96 my $code = $route->code;
178 30 50 33     324 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         78 $self->hook('before_validation')->($self);
188              
189             # Validation and coercion of declared params
190 30 50       170 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         103 $self->hook('after_validation')->($self);
197              
198             # Evaluate an endpoint
199 30         227 my $data = $code->($req->declared_params);
200 30 50       148 if (defined $data) {
201             # Delayed response
202 30 50       107 return $data if ref($data) eq 'CODE';
203              
204 30         102 $res->body($data);
205             }
206              
207 30         213 $self->hook('after')->($self);
208              
209 30         207 1;
210 30 50       57 } 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       88 if (ref($ret) eq 'CODE') {
222 0         0 return $ret;
223             }
224              
225 30         86 $self->finalize;
226             }
227              
228             # Finalize response
229             sub before_finalize {
230 30     30 0 46 my $self = shift;
231              
232 30 50       83 $self->res->status(HTTP_OK) unless $self->res->status;
233 30         175 $self->res->header('X-Framework' => 'Raisin ' . __PACKAGE__->VERSION);
234              
235 30 50       2016 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         86 $self->before_finalize;
243 30         67 $self->res->finalize;
244             }
245              
246             # Application defaults
247             sub default_format {
248 12     12 0 35 my ($self, $format) = @_;
249              
250 12 100       41 if ($format) {
251 3         8 $self->{default_format} = $format;
252             }
253              
254 12 100       81 $self->{default_format} || 'yaml';
255             }
256              
257             sub format {
258 12     12 0 37 my ($self, $format) = @_;
259              
260 12 100       36 if ($format) {
261 2         6 my @decoders = keys %{ $self->decoder->all };
  2         10  
262              
263 2 50       8 if (grep { lc($format) eq $_ } @decoders) {
  4         19  
264 2         6 $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         53 $self->{format};
273             }
274              
275             sub api_version {
276 38     38 1 88 my ($self, $version) = @_;
277 38 100       95 $self->{version} = $version if $version;
278             $self->{version}
279 38         147 }
280              
281             # Request and Response and shortcuts
282             sub req {
283 36     36 1 318 my ($self, $req) = @_;
284 36 100       435 $self->{req} = $req if $req;
285 36         625 $self->{req};
286             }
287              
288             sub res {
289 167     167 1 806 my ($self, $res) = @_;
290 167 100       432 $self->{res} = $res if $res;
291 167         942 $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__