File Coverage

blib/lib/Dancer/Plugin/Resource.pm
Criterion Covered Total %
statement 111 126 88.1
branch 23 34 67.6
condition 24 35 68.5
subroutine 21 23 91.3
pod n/a
total 179 218 82.1


line stmt bran cond sub pod time code
1             #
2             # This file is part of Dancer-Plugin-Resource
3             #
4             # This software is copyright (c) 2013 by Matthew Phillips.
5             #
6             # This is free software; you can redistribute it and/or modify it under
7             # the same terms as the Perl 5 programming language system itself.
8             #
9             package Dancer::Plugin::Resource;
10             # ABSTRACT: A plugin for writing declarative RESTful apps with Dancer
11             BEGIN {
12 8     8   2246942 our $VERSION = '1.131120'; # VERSION
13             }
14              
15 8     8   101 use strict;
  8         16  
  8         249  
16 8     8   44 use warnings;
  8         20  
  8         226  
17              
18 8     8   43 use Carp 'croak';
  8         14  
  8         453  
19 8     8   1119 use Dancer ':syntax';
  8         275955  
  8         45  
20 8     8   10210 use Dancer::Plugin;
  8         11764  
  8         658  
21 8     8   7209 use Lingua::EN::Inflect::Number;
  8         222623  
  8         65  
22              
23             our $RESOURCE_DEBUG = 0;
24              
25             my $content_types = {
26             json => 'application/json',
27             yml => 'text/x-yaml',
28             xml => 'application/xml',
29             };
30              
31             my %routes;
32              
33             # thanks leont
34             sub _function_exists {
35 8     8   769 no strict 'refs';
  8         18  
  8         16843  
36 146     146   326 my $funcname = shift;
37 146 100       140 return \&{$funcname} if defined &{$funcname};
  17         71  
  146         809  
38 129         490 return;
39             }
40              
41             sub prepare_serializer_for_format {
42 0     0   0 my $conf = plugin_setting;
43 0 0 0     0 my $serializers = (
44             ($conf && exists $conf->{serializers})
45             ? $conf->{serializers}
46             : { 'json' => 'JSON',
47             'yml' => 'YAML',
48             'xml' => 'XML',
49             'dump' => 'Dumper',
50             }
51             );
52              
53             hook 'before' => sub {
54 0     0   0 my $format = params->{'format'};
55 0 0       0 if (not defined $format) {
56 0         0 set serializer => 'Mutable';
57 0         0 return;
58             }
59              
60 0 0       0 return unless defined $format;
61              
62 0         0 my $serializer = $serializers->{$format};
63 0 0       0 unless (defined $serializer) {
64 0         0 return halt(
65             Dancer::Error->new(
66             code => 404,
67             message => "unsupported format requested: " . $format
68             )
69             );
70             }
71              
72 0         0 set serializer => $serializer;
73 0   0     0 my $ct = $content_types->{$format} || setting('content_type');
74 0         0 content_type $ct;
75 0         0 };
76             }
77             register prepare_serializer_for_format => \&prepare_serializer_for_format;
78              
79             register resource => sub {
80 8     8   2938 my ($resource, %options) = @_;
81              
82 8         18 my $params = ':id';
83 8         16 my ($old_prefix, $parent_prefix);
84              
85 8 50 33     125 unless ($options{skip_prepare_serializer} || ((caller)[1] =~ /^(?:t|xt)/)) {
86 0         0 prepare_serializer_for_format;
87             }
88              
89             # if this resource is a nested child resource, manage the prefix
90 8   100     70 $old_prefix = Dancer::App->current->prefix || '';
91 8         709 $parent_prefix = '';
92              
93 8 100 66     47 if ($options{parent} and $routes{$options{parent}}) {
94 2         10 prefix $parent_prefix = $routes{$options{parent}};
95             }
96             else {
97 6         17 $parent_prefix = $old_prefix;
98             }
99              
100             # create a default for the load funcs
101 8   100 17   241 $options{$_} ||= sub { undef } for (qw/load load_all/);
  17         82  
102              
103             # if member => 'foo' is passed, turn it into an array
104 8         22 for my $type (qw/member collection/) {
105 16 100 100     75 if ($options{$type} && ref $options{$type} eq '') {
106 1         4 $options{$type} = [$options{$type}];
107             }
108             }
109              
110             # by default take the singular resource as the param name (ie :user for users)
111 8         49 my ($singular_resource, $plural_resource) = (Lingua::EN::Inflect::Number::to_S($resource), $resource);
112              
113             # or if the user wants to override to take multiple params, ie /user/:foo/:bar/:baz
114             # allow it. This could be useful for composite key schemas
115 8 100       35698 if ( my $p = $options{params} ) {
116 1 50       5 $p = ref $p ? $p : [$p];
117 1         3 $params = join '/', map ":${_}", @{$p};
  1         7  
118             }
119             else {
120 7         29 $params = ":${singular_resource}_id";
121             }
122              
123 8         33 my ($package) = caller;
124              
125             # main resource endpoints
126             # CRUD
127 8         51 _post(
128             _endpoint(
129             path => $plural_resource,
130             params => '',
131             verbs => [qw/POST create/],
132             function => $singular_resource
133             )
134             );
135              
136 8         1621 _get(
137             _endpoint(
138             path => $plural_resource,
139             params => $params,
140             verbs => [qw/GET get read/],
141             loader => $options{load},
142             function => $singular_resource
143             )
144             );
145              
146 8         3186 _put(
147             _endpoint(
148             path => $plural_resource,
149             params => $params,
150             verbs => [qw/PUT update/],
151             loader => $options{load},
152             function => $singular_resource
153             )
154             );
155              
156 8         1548 _del(
157             _endpoint(
158             path => $plural_resource,
159             params => $params,
160             verbs => [qw/DELETE delete/],
161             loader => $options{load},
162             function => $singular_resource
163             )
164             );
165              
166 8         1603 _get(
167             _endpoint(
168             path => $plural_resource,
169             params => '',
170             verbs => [qw/INDEX index/],
171             loader => $options{load_all},
172             function => $singular_resource
173             )
174             );
175              
176             # member routes are actions on the given id. ie /users/:user_id/foo
177 8         8838 for my $member (@{$options{member}}) {
  8         32  
178 1         4 my $path = "${plural_resource}/$params/${member}";
179 1         3 my $member_param = "";
180              
181 1         6 _post(
182             _endpoint(
183             path => $path,
184             params => '',
185             verbs => [qw/POST create/],
186             loader => $options{load},
187             function => "${singular_resource}_${member}"
188             )
189             );
190              
191 1         262 _get(
192             _endpoint(
193             path => $path,
194             params => $member_param,
195             verbs => [qw/GET get read/],
196             loader => $options{load},
197             function => "${singular_resource}_${member}"
198              
199             )
200             );
201              
202 1         553 _put(
203             _endpoint(
204             path => $path,
205             params => $member_param,
206             verbs => [qw/PUT update/],
207             loader => $options{load},
208             function => "${singular_resource}_${member}"
209              
210             )
211             );
212              
213 1         239 _del(
214             _endpoint(
215             path => $path,
216             params => $member_param,
217             verbs => [qw/DELETE delete/],
218             loader => $options{load},
219             function => "${singular_resource}_${member}"
220              
221             )
222             );
223             }
224              
225             # collection routes are actions on the collection. ie /users/foo
226 8         239 for my $collection (@{$options{collection}}) {
  8         27  
227 1         3 my $path = "${plural_resource}/${collection}";
228              
229 1         6 _post(
230             _endpoint(
231             path => $path,
232             params => '',
233             verbs => [qw/POST create/],
234             loader => $options{load_all},
235             function => "${plural_resource}_${collection}"
236             )
237             );
238              
239 1         225 _get(
240             _endpoint(
241             path => $path,
242             params => '',
243             verbs => [qw/GET get read/],
244             loader => $options{load_all},
245             function => "${plural_resource}_${collection}"
246             )
247             );
248              
249 1         439 _put(
250             _endpoint(
251             path => $path,
252             params => '',
253             verbs => [qw/PUT update/],
254             loader => $options{load_all},
255             function => "${plural_resource}_${collection}"
256             )
257             );
258              
259 1         221 _del(
260             _endpoint(
261             path => $path,
262             params => '',
263             verbs => [qw/DELETE delete/],
264             loader => $options{load_all},
265             function => "${plural_resource}_${collection}"
266             )
267             );
268             }
269              
270             # save every defined resource if it is referred as a parent in a nested child resource
271 8         253 $routes{$resource} = "${parent_prefix}/${plural_resource}/${params}";
272              
273             # restore existing prefix if saved
274 8 100       69 prefix $old_prefix if $old_prefix;
275             };
276              
277 144 50   144   1513 sub _debug { $RESOURCE_DEBUG and print @_ }
278              
279             sub _post {
280 10     10   18 my ($route, $sub) = @_;
281 10         37 for ($route . '.:format', $route) {
282 20   100     2717 _debug("=> POST " .(Dancer::App->current->prefix||'').$_."\n");
283 20         69 post($_ => $sub);
284             }
285             }
286              
287             sub _get {
288 18     18   32 my ($route, $sub) = @_;
289 18         51 for ($route . '.:format', $route) {
290 36   100     7310 _debug("=> GET " .(Dancer::App->current->prefix||'').$_."\n");
291 36         110 get($_ => $sub);
292             }
293             }
294              
295             sub _put {
296 10     10   22 my ($route, $sub) = @_;
297 10         36 for ($route . '.:format', $route) {
298 20   100     2127 _debug("=> PUT " .(Dancer::App->current->prefix||'').$_."\n");
299 20         66 put($_ => $sub);
300             }
301             }
302              
303             sub _del {
304 10     10   17 my ($route, $sub) = @_;
305 10         34 for ($route . '.:format', $route) {
306 20   100     2125 _debug("=> DEL " .(Dancer::App->current->prefix||'').$_."\n");
307 20         67 del($_ => $sub);
308             }
309             }
310              
311             sub _endpoint {
312 48     48   253 my %opts = @_;
313 48         139 my ($function, $word, $params, $verbs, $load_func) = @opts{qw/function path params verbs loader/};
314              
315 48         90 my $package = caller(1);
316              
317 48         55 my $wrapped;
318 48         88 for my $verb (@$verbs) {
319             # allow both foo_GET and GET_foo
320 81   100     256 my $func = _function_exists("${package}::${verb}_${function}") ||
321             _function_exists("${package}::${function}_${verb}");
322              
323 81 100       227 if ($func) {
324 17         61 _debug("${package}::${verb}_${function} ");
325 17 100   25   75 $wrapped = sub { $func->($load_func ? $load_func->() : (), @_) };
  25         96012  
326              
327 17         34 last; # we only want to attach to the first successful verb
328             }
329             }
330              
331 48 100       106 if (not $wrapped) {
332 31         70 _debug("undef ");
333              
334             # if we've gotten this far, no route exists. use a default
335 31     2   111 $wrapped = sub { status_method_not_allowed('Method not allowed.'); };
  2         9779  
336             }
337              
338 48 100       146 my $route
339             = $params ? "/${word}/${params}"
340             : "/${word}";
341              
342 48         251 return ($route, $wrapped);
343             }
344              
345             register send_entity => sub {
346 20     20   41 my ($entity, $http_code) = @_;
347              
348 20   50     70 $http_code ||= 200;
349              
350 20         72 status($http_code);
351 20         612 $entity;
352             };
353              
354             my %http_codes = (
355              
356             # 1xx
357             100 => 'Continue',
358             101 => 'Switching Protocols',
359             102 => 'Processing',
360              
361             # 2xx
362             200 => 'OK',
363             201 => 'Created',
364             202 => 'Accepted',
365             203 => 'Non-Authoritative Information',
366             204 => 'No Content',
367             205 => 'Reset Content',
368             206 => 'Partial Content',
369             207 => 'Multi-Status',
370             210 => 'Content Different',
371              
372             # 3xx
373             300 => 'Multiple Choices',
374             301 => 'Moved Permanently',
375             302 => 'Found',
376             303 => 'See Other',
377             304 => 'Not Modified',
378             305 => 'Use Proxy',
379             307 => 'Temporary Redirect',
380             310 => 'Too many Redirect',
381              
382             # 4xx
383             400 => 'Bad Request',
384             401 => 'Unauthorized',
385             402 => 'Payment Required',
386             403 => 'Forbidden',
387             404 => 'Not Found',
388             405 => 'Method Not Allowed',
389             406 => 'Not Acceptable',
390             407 => 'Proxy Authentication Required',
391             408 => 'Request Time-out',
392             409 => 'Conflict',
393             410 => 'Gone',
394             411 => 'Length Required',
395             412 => 'Precondition Failed',
396             413 => 'Request Entity Too Large',
397             414 => 'Request-URI Too Long',
398             415 => 'Unsupported Media Type',
399             416 => 'Requested range unsatisfiable',
400             417 => 'Expectation failed',
401             418 => 'Teapot',
402             422 => 'Unprocessable entity',
403             423 => 'Locked',
404             424 => 'Method failure',
405             425 => 'Unordered Collection',
406             426 => 'Upgrade Required',
407             449 => 'Retry With',
408             450 => 'Parental Controls',
409              
410             # 5xx
411             500 => 'Internal Server Error',
412             501 => 'Not Implemented',
413             502 => 'Bad Gateway',
414             503 => 'Service Unavailable',
415             504 => 'Gateway Time-out',
416             505 => 'HTTP Version not supported',
417             507 => 'Insufficient storage',
418             509 => 'Bandwidth Limit Exceeded',
419             );
420              
421             for my $code (keys %http_codes) {
422             my $helper_name = lc($http_codes{$code});
423             $helper_name =~ s/[^\w]+/_/gms;
424             $helper_name = "status_${helper_name}";
425              
426             register $helper_name => sub {
427 20 100 66 20   7607 if ($code >= 400 && ref $_[0] eq '') {
428 7         44 send_entity({error => $_[0]}, $code);
429             }
430             else {
431 13         131 send_entity($_[0], $code);
432             }
433             };
434             }
435              
436             register_plugin;
437             1;
438              
439              
440             __END__