File Coverage

blib/lib/Raisin.pm
Criterion Covered Total %
statement 140 166 84.3
branch 32 48 66.6
condition 4 14 28.5
subroutine 33 35 94.2
pod 7 19 36.8
total 216 282 76.6


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   643890 use strict;
  12         61  
  12         379  
6 12     12   52 use warnings;
  12         18  
  12         550  
7              
8             package Raisin;
9             $Raisin::VERSION = '0.94';
10 12     12   60 use Carp qw(croak carp longmess);
  12         17  
  12         586  
11 12     12   3361 use HTTP::Status qw(:constants);
  12         32472  
  12         4202  
12 12     12   4949 use Plack::Response;
  12         106069  
  12         337  
13 12     12   3824 use Plack::Util;
  12         74937  
  12         338  
14              
15 12     12   4395 use Raisin::Request;
  12         38  
  12         382  
16 12     12   4831 use Raisin::Routes;
  12         30  
  12         335  
17 12     12   61 use Raisin::Util;
  12         20  
  12         211  
18              
19 12     12   4511 use Raisin::Middleware::Formatter;
  12         26  
  12         351  
20 12     12   4368 use Raisin::Encoder;
  12         25  
  12         312  
21 12     12   3906 use Raisin::Decoder;
  12         26  
  12         324  
22              
23 12         117 use Plack::Util::Accessor qw(
24             middleware
25             mounted
26             routes
27              
28             decoder
29             encoder
30 12     12   62 );
  12         25  
31              
32             sub new {
33 9     9 0 9340 my ($class, %args) = @_;
34              
35 9         40 my $self = bless { %args }, $class;
36              
37 9         53 $self->middleware({});
38 9         115 $self->mounted([]);
39 9         93 $self->routes(Raisin::Routes->new);
40              
41 9         130 $self->decoder(Raisin::Decoder->new);
42 9         73 $self->encoder(Raisin::Encoder->new);
43              
44 9         636 $self;
45             }
46              
47             sub mount_package {
48 2     2 0 4 my ($self, $package) = @_;
49 2         3 push @{ $self->{mounted} }, $package;
  2         6  
50 2         6 Plack::Util::load_class($package);
51             }
52              
53             sub load_plugin {
54 10     10 0 35 my ($self, $name, @args) = @_;
55 10 100       39 return if $self->{loaded_plugins}{$name};
56              
57 8         44 my $class = Plack::Util::load_class($name, 'Raisin::Plugin');
58 8         120 my $module = $self->{loaded_plugins}{$name} = $class->new($self);
59              
60 8         32 $module->build(@args);
61             }
62              
63             sub add_middleware {
64 2     2 0 8 my ($self, $name, @args) = @_;
65 2         12 $self->{middleware}{$name} = \@args;
66             }
67              
68             # Routes
69             sub add_route {
70 51     51 0 236 my ($self, %params) = @_;
71 51         126 $self->routes->add(%params);
72             }
73              
74             # Resource description
75             sub resource_desc {
76 9     9 0 25 my ($self, $ns, $desc) = @_;
77 9 100       28 $self->{resource_desc}{$ns} = $desc if $desc;
78 9         22 $self->{resource_desc}{$ns};
79             }
80              
81             # Hooks
82             sub hook {
83 128     128 0 201 my ($self, $name) = @_;
84 128 100   124   711 $self->{hooks}{$name} || sub {};
85             }
86              
87             sub add_hook {
88 4     4 0 8 my ($self, $name, $code) = @_;
89 4         11 $self->{hooks}{$name} = $code;
90             }
91              
92             # Application
93             sub run {
94 7     7 1 20 my $self = shift;
95 7     32   42 my $psgi = sub { $self->psgi(@_) };
  32         450  
96              
97 7         25 $self->{allowed_methods} = $self->generate_allowed_methods;
98              
99             # Add middleware
100 7         16 for my $class (keys %{ $self->{middleware} }) {
  7         23  
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         3019 my $args = $self->{middleware}{$class};
106 2         24 $psgi = $mw->wrap($psgi, @$args);
107             }
108              
109 7         263 $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         439 $self->load_plugin('Logger', fallback => 1);
120              
121 7         30 return $psgi;
122             }
123              
124             sub generate_allowed_methods {
125 7     7 0 16 my $self = shift;
126              
127 7         12 my %allowed_methods_by_endpoint;
128              
129             # `options` for each `path`
130 7         12 for my $path (keys %{ $self->routes->list }) {
  7         20  
131             my $methods = join ', ',
132 11         56 sort(keys(%{ $self->routes->list->{$path} }), 'OPTIONS');
  11         23  
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         131 );
142              
143 11         77 $allowed_methods_by_endpoint{$path} = $methods;
144             }
145              
146 7         39 \%allowed_methods_by_endpoint;
147             }
148              
149             sub psgi {
150 32     32 0 67 my ($self, $env) = @_;
151              
152             # New for each response
153 32         186 my $req = $self->req(Raisin::Request->new($env));
154 32         180 my $res = $self->res(Plack::Response->new);
155              
156             # Generate API description
157 32 100       128 if ($self->can('swagger_build_spec')) {
158 19         42 $self->swagger_build_spec;
159             }
160              
161             my $ret = eval {
162 32         80 $self->hook('before')->($self);
163              
164             # Find a route
165 32         354 my $route = $self->routes->find($req->method, $req->path);
166             # The requested path exists but requested method not
167 32 50 33     153 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 32         97 my $code = $route->code;
178 32 50 33     257 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 32         77 $self->hook('before_validation')->($self);
188              
189             # Validation and coercion of declared params
190 32 50       149 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 32         101 $self->hook('after_validation')->($self);
197              
198             # Evaluate an endpoint
199 32         187 my $data = $code->($req->declared_params);
200 32 50       118 if (defined $data) {
201             # Delayed response
202 32 50       71 return $data if ref($data) eq 'CODE';
203              
204 32         92 $res->body($data);
205             }
206              
207 32         178 $self->hook('after')->($self);
208              
209 32         166 1;
210 32 50       50 } 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 32 50       69 if (ref($ret) eq 'CODE') {
222 0         0 return $ret;
223             }
224              
225 32         96 $self->finalize;
226             }
227              
228             # Finalize response
229             sub before_finalize {
230 32     32 0 52 my $self = shift;
231              
232 32 50       77 $self->res->status(HTTP_OK) unless $self->res->status;
233 32   50     144 $self->res->header('X-Framework' => 'Raisin ' . (__PACKAGE__->VERSION || 'dev'));
234              
235 32 50       1579 if ($self->api_version) {
236 0         0 $self->res->header('X-API-Version' => $self->api_version);
237             }
238             }
239              
240             sub finalize {
241 32     32 0 55 my $self = shift;
242 32         81 $self->before_finalize;
243 32         54 $self->res->finalize;
244             }
245              
246             # Application defaults
247             sub default_format {
248 12     12 1 37 my ($self, $format) = @_;
249              
250 12 100       39 if ($format) {
251 3         7 $self->{default_format} = $format;
252             }
253              
254 12 100       72 $self->{default_format} || 'yaml';
255             }
256              
257             sub format {
258 12     12 1 29 my ($self, $format) = @_;
259              
260 12 100       36 if ($format) {
261 2         5 my @decoders = keys %{ $self->decoder->all };
  2         12  
262              
263 2 50       8 if (grep { lc($format) eq $_ } @decoders) {
  6         17  
264 2         7 $self->{format} = lc $format;
265 2         11 $self->default_format(lc $format);
266             }
267             else {
268 0         0 carp 'Invalid format, choose one of: ', join(', ', @decoders);
269             }
270             }
271              
272 12         49 $self->{format};
273             }
274              
275             sub api_version {
276 40     40 1 88 my ($self, $version) = @_;
277 40 100       87 $self->{version} = $version if $version;
278             $self->{version}
279 40         109 }
280              
281             # Request and Response and shortcuts
282             sub req {
283 38     38 1 282 my ($self, $req) = @_;
284 38 100       304 $self->{req} = $req if $req;
285 38         648 $self->{req};
286             }
287              
288             sub res {
289 177     177 1 703 my ($self, $res) = @_;
290 177 100       361 $self->{res} = $res if $res;
291 177         825 $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__