File Coverage

blib/lib/Dancer/Plugin/CRUD.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Dancer::Plugin::CRUD;
2              
3 2     2   41027 use Modern::Perl;
  2         37966  
  2         13  
4              
5             =head1 NAME
6              
7             Dancer::Plugin::CRUD - A plugin for writing RESTful apps with Dancer
8              
9             =head1 VERSION
10              
11             Version 1.03
12              
13             =cut
14              
15             our $VERSION = '1.03';
16              
17             =head1 DESCRIPTION
18              
19             This plugin is derived from L and helps you write a RESTful webservice with Dancer.
20              
21             =head1 SYNOPSYS
22              
23             package MyWebService;
24            
25             use Dancer;
26             use Dancer::Plugin::CRUD;
27            
28             prepare_serializer_for_format;
29            
30             my $userdb = My::UserDB->new(...);
31            
32             resource('user',
33             'read' => sub { $userdb->find(captures()->{'user_id'}) }
34             );
35            
36             # curl http://mywebservice/user/42.json
37             { "id": 42, "name": "John Foo", email: "john.foo@example.com"}
38            
39             # curl http://mywebservice/user/42.yml
40             --
41             id: 42
42             name: "John Foo"
43             email: "john.foo@example.com"
44              
45             =cut
46              
47 2     2   366 use Carp 'croak';
  2         5  
  2         100  
48 2     2   963 use Dancer ':syntax';
  0            
  0            
49             use Dancer::Plugin;
50             use Sub::Name;
51             use Text::Pluralize;
52             use Validate::Tiny ();
53              
54             our $SUFFIX = '_id';
55              
56             my $content_types = {
57             json => 'application/json',
58             yml => 'text/x-yaml',
59             xml => 'application/xml',
60             dump => 'text/x-perl',
61             jsonp => 'text/javascript',
62             };
63              
64             my %triggers_map = (
65             get => \&get,
66             index => \&get,
67             read => \&get,
68            
69             post => \&post,
70             create => \&post,
71            
72             put => \&put,
73             update => \&put,
74            
75             del => \&del,
76             delete => \&del,
77            
78             patch => \&patch,
79             );
80              
81             my %alt_syntax = (
82             get => 'read',
83             post => 'create',
84             put => 'update',
85             del => 'delete',
86             );
87              
88             my %http_codes = (
89              
90             # 1xx
91             100 => 'Continue',
92             101 => 'Switching Protocols',
93             102 => 'Processing',
94              
95             # 2xx
96             200 => 'OK',
97             201 => 'Created',
98             202 => 'Accepted',
99             203 => 'Non-Authoritative Information',
100             204 => 'No Content',
101             205 => 'Reset Content',
102             206 => 'Partial Content',
103             207 => 'Multi-Status',
104             210 => 'Content Different',
105              
106             # 3xx
107             300 => 'Multiple Choices',
108             301 => 'Moved Permanently',
109             302 => 'Found',
110             303 => 'See Other',
111             304 => 'Not Modified',
112             305 => 'Use Proxy',
113             307 => 'Temporary Redirect',
114             310 => 'Too many Redirect',
115              
116             # 4xx
117             400 => 'Bad Request',
118             401 => 'Unauthorized',
119             402 => 'Payment Required',
120             403 => 'Forbidden',
121             404 => 'Not Found',
122             405 => 'Method Not Allowed',
123             406 => 'Not Acceptable',
124             407 => 'Proxy Authentication Required',
125             408 => 'Request Time-out',
126             409 => 'Conflict',
127             410 => 'Gone',
128             411 => 'Length Required',
129             412 => 'Precondition Failed',
130             413 => 'Request Entity Too Large',
131             414 => 'Request-URI Too Long',
132             415 => 'Unsupported Media Type',
133             416 => 'Requested range unsatisfiable',
134             417 => 'Expectation failed',
135             418 => 'Teapot',
136             422 => 'Unprocessable entity',
137             423 => 'Locked',
138             424 => 'Method failure',
139             425 => 'Unordered Collection',
140             426 => 'Upgrade Required',
141             449 => 'Retry With',
142             450 => 'Parental Controls',
143              
144             # 5xx
145             500 => 'Internal Server Error',
146             501 => 'Not Implemented',
147             502 => 'Bad Gateway',
148             503 => 'Service Unavailable',
149             504 => 'Gateway Time-out',
150             505 => 'HTTP Version not supported',
151             507 => 'Insufficient storage',
152             509 => 'Bandwidth Limit Exceeded',
153             );
154              
155             our $default_serializer;
156             my $stack = [];
157              
158             sub _generate_sub($) {
159             my %options = %{ shift() };
160            
161             my $resname = $options{stack}->[-1]->{resname};
162              
163             my $rules = [ map { $_->{validation_rules}->{generic} } grep { exists $_->{validation_rules} } reverse @{ $options{stack} } ];
164            
165             if (@$rules > 0) {
166             push @$rules, $options{stack}->[-1]->{validation_rules}->{$options{action}}
167             if exists $options{stack}->[-1]->{validation_rules}->{$options{action}};
168            
169             $rules = {
170             fields => [ map { ( @{ $_->{fields} } ) } grep { exists $_->{fields} } @$rules ],
171             checks => [ map { ( @{ $_->{checks} } ) } grep { exists $_->{checks} } @$rules ],
172             filters => [ map { ( @{ $_->{filters} } ) } grep { exists $_->{filters} } @$rules ],
173             };
174             } else {
175             $rules = undef;
176             }
177            
178             my $chain = [ map { $_->{chain} } grep { exists $_->{chain} } @{ $options{stack} } ];
179            
180             my @idfields = map { $_->{resname}.$SUFFIX }
181             grep { (($options{action} =~ m'^(index|create)$') and ($_->{resname} eq $resname)) ? 0 : 1 }
182             @{ $options{stack} };
183            
184             my $subname = join('_', $resname, $options{action});
185            
186             return subname($subname, sub {
187             if (defined $rules) {
188             my $input = {
189             %{ params('query') },
190             %{ params('body') },
191             %{ captures() || {} }
192             };
193             my $result = Validate::Tiny->new($input, {
194             %$rules,
195             fields => [
196             @idfields,
197             @{ $rules->{fields} }
198             ]
199             });
200             unless ($result->success) {
201             status(400);
202             return { error => $result->error };
203             }
204             var validate => $result;
205             }
206            
207             {
208             my @chain = @$chain;
209             unless ($options{action} ~~ [qw[ read update delete patch ]]) {
210             pop @chain;
211             }
212             $_->() for @chain;
213             }
214            
215             my @ret = $options{coderef}->(map { $_->{resname} } @{ $options{stack} });
216            
217             if (@ret and defined $ret[0] and ref $ret[0] eq '' and $ret[0] =~ m{^\d{3}$}) {
218             # return ($http_status_code, ...)
219             if ($ret[0] >= 400) {
220             # return ($http_error_code, $error_message)
221             status($ret[0]);
222             return { error => $ret[1] };
223             } else {
224             # return ($http_success_code, $payload)
225             status($ret[0]);
226             return $ret[1];
227             }
228             } elsif (status eq '200') {
229             # http status wasn't changed yet
230             given ($options{action}) {
231             when ('create') { status(201); }
232             when ('update') { status(202); }
233             when ('delete') { status(202); }
234             }
235             }
236             # return payload
237             return (wantarray ? @ret : $ret[0]);
238             });
239             }
240              
241             sub _prefix {
242             my ($prefix, $cb) = @_;
243            
244             my $app = Dancer::App->current;
245              
246             my $app_prefix = defined $app->app_prefix ? $app->app_prefix : "";
247             my $previous = Dancer::App->current->prefix;
248              
249             if ($app->on_lexical_prefix) {
250             if (ref $previous eq 'Regexp') {
251             $app->prefix(qr/${previous}${prefix}/);
252             } else {
253             my $previous_ = quotemeta($previous);
254             $app->prefix(qr/${previous_}${prefix}/);
255             }
256             } else {
257             if (ref $app_prefix eq 'Regexp') {
258             $app->prefix(qr/${app_prefix}${prefix}/);
259             } else {
260             my $app_prefix_ = quotemeta($app_prefix);
261             $app->prefix(qr/${app_prefix_}${prefix}/);
262             }
263             }
264            
265             if (ref($cb) eq 'CODE') {
266             $app->incr_lexical_prefix;
267             eval { $cb->() };
268             my $e = $@;
269             $app->dec_lexical_prefix;
270             $app->prefix($previous);
271             die $e if $e;
272             }
273             }
274              
275              
276              
277             =head1 METHODS
278              
279             =head2 C<< prepare_serializer_for_format >>
280              
281             When this pragma is used, a before filter is set by the plugin to automatically
282             change the serializer when a format is detected in the URI.
283              
284             That means that each route you define with a B<:format> token will trigger a
285             serializer definition, if the format is known.
286              
287             This lets you define all the REST actions you like as regular Dancer route
288             handlers, without explicitly handling the outgoing data format.
289              
290             =cut
291              
292             register prepare_serializer_for_format => sub () {
293             my $conf = plugin_setting;
294             my $serializers = {
295             'json' => 'JSON',
296             'jsonp' => 'JSONP',
297             'yml' => 'YAML',
298             'xml' => 'XML',
299             'dump' => 'Dumper',
300             (exists $conf->{serializers} ? %{$conf->{serializers}} : ())
301             };
302              
303             hook(before => sub {
304             # remember what was there before
305             $default_serializer ||= setting('serializer');
306              
307             my $format = defined captures() ? captures->{format} : undef;
308             $format ||= param('format') or return;
309              
310             my $serializer = $serializers->{$format} or return halt(Dancer::Error->new(
311             code => 404,
312             title => "unsupported format requested",
313             message => "unsupported format requested: " . $format
314             )->render);
315              
316             set(serializer => $serializer);
317              
318             # check if we were supposed to deserialize the request
319             Dancer::Serializer->process_request(Dancer::SharedData->request);
320              
321             content_type($content_types->{$format} || setting('content_type'));
322             });
323              
324             hook(after => sub {
325             # put it back the way it was
326             set(serializer => $default_serializer);
327             });
328             };
329              
330             =head2 C<< resource >>
331              
332             This keyword lets you declare a resource your application will handle.
333              
334             Derived from L, this method has rewritten to provide a more slightly convention. C has been renamed to C and three new actions has been added: C, C, C and C
335              
336             Also, L is applied to resource name with count=1 for singular variant and count=2 for plural variant. If you don't provide a singular/plural variant (i.e. resource name contains parenthesis) the singular and the plural becomes same.
337              
338             The id name is derived from singular resource name, appended with C<_id>.
339              
340             resource 'user(s)' =>
341             index => sub { ... }, # return all users
342             read => sub { ... }, # return user where id = captures->{user_id}
343             create => sub { ... }, # create a new user with params->{user}
344             delete => sub { ... }, # delete user where id = captures->{user_id}
345             update => sub { ... }, # update user with params->{user}
346             patch => sub { ... }, # patches user with params->{user}
347             prefix => sub {
348             # prefixed resource in plural
349             # routes are only possible with regex!
350             get qr{/foo} => sub { ... },
351             },
352             prefix_id => sub {
353             # prefixed resource in singular with id
354             # captures->{user_id}
355             # routes are only possible with regex!
356             get qr{/bar} => sub { ... },
357             };
358              
359             # this defines the following routes:
360             # prefix_id =>
361             # GET /user/:user_id/bar
362             # prefix =>
363             # GET /users/foo
364             # index =>
365             # GET /users.:format
366             # GET /users
367             # create =>
368             # POST /user.:format
369             # POST /user
370             # read =>
371             # GET /user/:id.:format
372             # GET /user/:id
373             # delete =>
374             # DELETE /user/:id.:format
375             # DELETE /user/:id
376             # update =>
377             # PUT /user/:id.:format
378             # PUT /user/:id
379             # patch =>
380             # PATCH /user.:format
381             # PATCH /user
382              
383             The routes are created in the above order.
384              
385             Returns a hash with arrayrefs of all created L objects.
386            
387             Hint: resources can be stacked with C/C:
388              
389             resource foo =>
390             prefix => sub {
391             get '/bar' => sub {
392             return 'Hi!'
393             };
394             }, # GET /foo/bar
395             prefix_id => sub {
396             get '/bar' => sub {
397             return 'Hey '.captures->{foo_id}
398             }; # GET /foo/123/bar
399             resource bar =>
400             read => sub {
401             return 'foo is '
402             . captures->{foo_id}
403             .' and bar is '
404             . captures->{bar_id}
405             }
406             }; # GET /foo/123/bar/456
407             };
408              
409             When is return value is a HTTP status code (three digits), C is applied to it. A second return value may be the value to be returned to the client itself:
410              
411             sub {
412             return 200
413             };
414            
415             sub {
416             return 404 => 'This object has not been found.'
417             }
418            
419             sub {
420             return 201 => { ... }
421             };
422            
423             The default HTTP status code ("200 OK") differs in some actions: C response with "201 Created", C and C response with "202 Accepted".
424              
425             =head3 Change of suffix
426              
427             The appended suffix, C<_id> for default, can be changed by setting C<< $Dancer::Plugin::CRUD::SUFFIX >>. This affects both captures names and the suffix of parameterized C method:
428              
429             $Dancer::Plugin::CRUD::SUFFIX = 'Id';
430             resource 'User' => prefixId => sub { return captures->{'UserId'} };
431              
432             =head3 Automatic validation of parameters
433              
434             Synopsis:
435              
436             resource foo =>
437             validation => {
438             generic => {
439             checks => [
440             foo_id => Validate::Tiny::is_like(qr{^\d+})
441             ]
442             },
443             },
444             read => sub {
445             $foo_id = var('validate')->data('foo_id');
446             },
447             ;
448              
449             The keyword C specifies rules for L.
450              
451             The parameter input resolves to following order: C, C, C.
452              
453             The rules and the result of C are applied to C and stored in C.
454              
455             The hashref C accepts seven keywords:
456              
457             =over 4
458              
459             =item I
460              
461             These are generic rules, used in every action. For the actions I and I, the fields I<<< C<< $resource >>_id >>> are ignored, since they aren't needed.
462              
463             =item I, I, I, I, I
464              
465             These rules are merged together with I.
466              
467             =item I, I
468              
469             These rules are merged together with I, but they can only used when C is used in the prefix subs.
470              
471             =item I
472              
473             These rules apply when in a prefix or prefix_id routine the I keyword is used:
474              
475             resource foo =>
476             validation => {
477             wrap => {
478             GET => {
479             bar => {
480             fields => [qw[ name ]]
481             }
482             }
483             }
484             },
485             prefix => sub {
486             wrap GET => bar => sub { ... }
487             };
488              
489             =back
490              
491             The id-fields (I<<< C<< $resource >>_id >>>, ...) are automatically prepended to the I param of Validate::Tiny. There is no need to define them especially.
492              
493             An advantage is the feature of stacking resources and to define validation rules only once.
494              
495             Example:
496              
497             resource foo =>
498             validation => {
499             generic => {
500             checks => [
501             foo_id => Validate::Tiny::is_like(qr{^\d+})
502             ]
503             },
504             },
505             prefix_id => sub {
506             resource bar =>
507             validation => {
508             generic => {
509             checks => [
510             bar_id => Validate::Tiny::is_like(qr{^\d+})
511             ]
512             },
513             },
514             read => sub {
515             $foo_id = var('validate')->data('foo_id');
516             $bar_id = var('validate')->data('foo_id');
517             },
518             ;
519             },
520             ;
521              
522             =head3 Chaining actions together
523              
524             To avoid redundant code, the keyword I may used to define a coderef executing every times the resource (and possible parent resources) is triggered, irrespective of the method.
525              
526             Example:
527              
528             resource foo =>
529             chain => sub { var onetwothree => 123 },
530             index => sub { return var('onetwothree') }
531             prefix_id => sub {
532             resource bar =>
533             chain => sub { var fourfivesix => 456 },
534             index => sub { return var('onetwothree').var('fourfivesix') },
535             ;
536             },
537             ;
538              
539             When resource I is triggered, the variable C is set to 123. When resource I is triggered, the variable C is set to 123 and, of course, C is set to 456.
540              
541             This is useful to obtain parent objects from DB and store it into the var stack.
542              
543             B: This feature may change in a future release.
544            
545             =cut
546              
547             register(resource => sub ($%) {
548             my $resource = my $resource1 = my $resource2 = shift;
549             my %triggers = @_;
550            
551             {
552             my $c = quotemeta '()|{}';
553             if ($resource =~ m{[$c]}) {
554             $resource1 = pluralize($resource1, 1);
555             $resource2 = pluralize($resource2, 2);
556             }
557             }
558            
559             my %options;
560             push @$stack => \%options;
561            
562             $options{resname} = $resource1;
563            
564             my $altsyntax = 0;
565             if (exists $triggers{altsyntax}) {
566             $altsyntax = delete $triggers{altsyntax};
567             }
568            
569             my $idregex = qr{[^\/\.\:\?]+};
570              
571             if (exists $triggers{idregex}) {
572             $idregex = delete $triggers{idregex};
573             }
574            
575             $options{prefix} = qr{/\Q$resource2\E};
576             $options{prefix_id} = qr{/\Q$resource1\E/(?<$resource1$SUFFIX>$idregex)};
577            
578             if (exists $triggers{validation}) {
579             $options{validation_rules} = delete $triggers{validation};
580             }
581            
582             if (exists $triggers{chain}) {
583             $options{chain} = delete $triggers{chain};
584             }
585            
586             if (exists $triggers{'prefix'.$SUFFIX}) {
587             my $subref = delete $triggers{'prefix'.$SUFFIX};
588             $options{prefixed_with_id} = 1;
589             my @prefixes = map { $_->{prefixed_with_id} ? $_->{prefix_id} : $_->{prefix} } grep { exists $_->{prefix} } @$stack;
590             local $" = '';
591             _prefix(qr{@prefixes}, $subref);
592             delete $options{prefixed_with_id};
593             }
594              
595             if (exists $triggers{prefix}) {
596             my $subref = delete $triggers{'prefix'};
597             $options{prefixed_with_id} = 0;
598             my @prefixes = map { $_->{prefixed_with_id} ? $_->{prefix_id} : $_->{prefix} } grep { exists $_->{prefix} } @$stack;
599             local $" = '';
600             _prefix(qr{@prefixes}, $subref);
601             delete $options{prefixed_with_id};
602             }
603            
604             my %routes;
605              
606             foreach my $action (qw(index create read delete update patch)) {
607             next unless exists $triggers{$action};
608              
609             my $route;
610            
611             given ($action) {
612             when ('index') {
613             $route = qr{/\Q$resource2\E};
614             }
615             when ('create') {
616             $route = qr{/\Q$resource1\E};
617             }
618             default {
619             $route = qr{/\Q$resource1\E/(?<$resource1$SUFFIX>$idregex)};
620             }
621             }
622            
623             my $sub = _generate_sub({
624             stack => $stack,
625             action => $action,
626             coderef => $triggers{$action},
627             });
628            
629             $routes{$action} = [];
630            
631             if ($altsyntax) {
632             push @{$routes{$action}} => $triggers_map{ get }->(qr{$route/\Q$action\E\.(?json|jsonp|yml|xml|dump)} => $sub);
633             push @{$routes{$action}} => $triggers_map{ get }->(qr{$route/\Q$action\E} => $sub);
634             }
635             push @{$routes{$action}} => $triggers_map{$action}->(qr{$route\.(?json|jsonp|yml|xml|dump)} => $sub);
636             push @{$routes{$action}} => $triggers_map{$action}->( $route => $sub);
637             }
638            
639             pop @$stack;
640            
641             return %routes;
642             });
643              
644             =head2 C<< wrap >>
645              
646             This keyword wraps validation rules and format accessors. For return values see C.
647              
648             Synopsis:
649              
650             resource foo =>
651             prefix_id => sub {
652             wrap GET => bar => sub {
653             # same as get('/bar', sub { ... });
654             # and get('/bar.:format', sub { ... });
655             # var('validate') is also availble,
656             # when key 'validation' is defined
657             };
658             },
659             ;
660              
661             I uses the same wrapper as for the actions in I. Any beviour there also applies here. For a better explaination, these resolves to the same routes:
662              
663             resource foo => read => sub { ... };
664             wrap read => foo => sub { ... };
665              
666             The first argument is an CRUD action (I, I, I, I, I) or a HTTP method (I, I, I, I, I) and is case-insensitve. The second argument is a route name. A leading slash will be prepended if the route contains to slashes. The third argument is the well known coderef.
667              
668             Please keep in mind that I creates two routes: I<<< /C<< $route >> >>> and I<<< /C<< $route >>.:format >>>.
669              
670             Returns a list of all created L objects.
671              
672             =cut
673              
674             register(wrap => sub($$$) {
675             my ($action, $route, $coderef) = @_;
676            
677             my @route = grep { defined and length } split m{/+}, $route;
678            
679             my $parent = @$stack ? $stack->[-1] : undef;
680             foreach my $route (@route) {
681             push @$stack => {
682             resname => $route
683             };
684             }
685            
686             if (defined $parent) {
687             if (exists $parent->{validation_rules} and
688             exists $parent->{validation_rules}->{wrap} and
689             exists $parent->{validation_rules}->{wrap}->{$action} and
690             exists $parent->{validation_rules}->{wrap}->{$action}->{$route}
691             ) {
692             $stack->[-1]->{validation_rules} = { lc($action) => $parent->{validation_rules}->{wrap}->{$action}->{$route} };
693             }
694             }
695            
696             my $sub = _generate_sub({
697             action => lc($action),
698             stack => $stack,
699             coderef => $coderef,
700             });
701            
702             pop @$stack for @route;
703            
704             my @routes;
705            
706             push @routes => $triggers_map{lc($action)}->(qr{/\Q$route\E\.(?json|jsonp|yml|xml|dump)} => $sub);
707             push @routes => $triggers_map{lc($action)}->(qr{/\Q$route\E} => $sub);
708            
709             return @routes;
710             });
711              
712             =head2 helpers
713              
714             Some helpers are available. This helper will set an appropriate HTTP status for you.
715              
716             =head3 status_ok
717              
718             status_ok({users => {...}});
719              
720             Set the HTTP status to 200
721              
722             =head3 status_created
723              
724             status_created({users => {...}});
725              
726             Set the HTTP status to 201
727              
728             =head3 status_accepted
729              
730             status_accepted({users => {...}});
731              
732             Set the HTTP status to 202
733              
734             =head3 status_bad_request
735              
736             status_bad_request("user foo can't be found");
737              
738             Set the HTTP status to 400. This function as for argument a scalar that will be used under the key B.
739              
740             =head3 status_not_found
741              
742             status_not_found("users doesn't exists");
743              
744             Set the HTTP status to 404. This function as for argument a scalar that will be used under the key B.
745              
746             =cut
747              
748             register send_entity => sub {
749             # entity, status_code
750             status($_[1] || 200);
751             $_[0];
752             };
753              
754             for my $code (keys %http_codes) {
755             my $helper_name = lc($http_codes{$code});
756             $helper_name =~ s/[^\w]+/_/gms;
757             $helper_name = "status_${helper_name}";
758              
759             register $helper_name => sub {
760             if ($code >= 400) {
761             send_entity({error => $_[0]}, $code);
762             }
763             else {
764             send_entity($_[0], $code);
765             }
766             };
767             }
768              
769             =head1 LICENCE
770              
771             This module is released under the same terms as Perl itself.
772              
773             =head1 AUTHORS
774              
775             This module has been rewritten by David Zurborg C<< >>, based on code written by Alexis Sukrieh C<< >> and Franck Cuny.
776              
777             =head1 SEE ALSO
778              
779             L
780             L
781             L
782             L
783              
784             =head1 AUTHORS
785              
786             =over 4
787              
788             =item *
789              
790             David Zurborg
791              
792             =item *
793              
794             Alexis Sukrieh (Author of Dancer::Plugin::REST)
795              
796             =item *
797              
798             Franck Cuny (Author of Dancer::Plugin::REST)
799              
800             =back
801              
802             =head1 BUGS
803              
804             Please report any bugs or feature requests trough my project management tool
805             at L. I
806             will be notified, and then you'll automatically be notified of progress on
807             your bug as I make changes.
808              
809             =head1 SUPPORT
810              
811             You can find documentation for this module with the perldoc command.
812              
813             perldoc Dancer::Plugin::CRUD
814              
815             You can also look for information at:
816              
817             =over 4
818              
819             =item * Redmine: Homepage of this module
820              
821             L
822              
823             =item * RT: CPAN's request tracker
824              
825             L
826              
827             =item * AnnoCPAN: Annotated CPAN documentation
828              
829             L
830              
831             =item * CPAN Ratings
832              
833             L
834              
835             =item * Search CPAN
836              
837             L
838              
839             =back
840              
841             =head1 COPYRIGHT AND LICENSE
842              
843             This software is copyright (c) 2014 by David Zurborg .
844              
845             This is free software; you can redistribute it and/or modify it under
846             the same terms as the Perl 5 programming language system itself.
847              
848             =cut
849              
850             register_plugin;
851             1;