File Coverage

blib/lib/Mojolicious/Plugin/Restify.pm
Criterion Covered Total %
statement 91 105 86.6
branch 36 58 62.0
condition 22 51 43.1
subroutine 8 11 72.7
pod 1 1 100.0
total 158 226 69.9


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Restify;
2 2     2   46097 use Mojo::Base 'Mojolicious::Plugin';
  2         4  
  2         13  
3              
4 2     2   2092 use Mojo::Util qw(camelize);
  2         76021  
  2         3242  
5              
6             our $VERSION = '0.07';
7              
8             sub register {
9 1     1 1 27 my ($self, $app, $conf) = @_;
10              
11 1   50     5 $conf //= {};
12              
13             # default HTTP method to instance method mappings for collections
14             $conf->{collection_method_map} //= {
15 1   50     7 get => 'list',
16             post => 'create',
17             };
18              
19             # default HTTP method to instance method mappings for resource elements
20             $conf->{element_method_map} //= {
21 1   50     8 delete => 'delete',
22             get => 'read',
23             patch => 'patch',
24             put => 'update',
25             };
26              
27             # over defaults to the standard route condition check (allow all)
28 1   50     2 $conf->{over} //= 'standard';
29              
30             # resource_lookup methods are added to element resource routes by default
31 1   50     4 $conf->{resource_lookup} //= 1;
32              
33             # When adding route conditions, warn developers if the exported conditions
34             # already exist.
35 1 50       5 if (exists $app->routes->conditions->{int}) {
36 0         0 $app->log->debug("The int route condition already exists, skipping");
37             }
38             else {
39             $app->routes->add_condition(
40             int => sub {
41 2     2   18766 my ($r, $c, $captures, $pattern) = @_;
42             my $int
43             = defined $pattern
44             ? ($captures->{$pattern} // $captures->{int})
45 2 50 33     9 : ($captures->{int} // '');
      0        
46              
47 2 50       14 return 1 if $int =~ /^\d+$/;
48             }
49 1         11 );
50             }
51              
52 1 50       14 if (exists $app->routes->conditions->{standard}) {
53 0         0 $app->log->debug("The standard route condition already exists, skipping");
54             }
55             else {
56 1     0   5 $app->routes->add_condition(standard => sub {1});
  0         0  
57             }
58              
59 1 50       8 if (exists $app->routes->conditions->{uuid}) {
60 0         0 $app->log->debug("The uuid route condition already exists, skipping");
61             }
62             else {
63             $app->routes->add_condition(
64             uuid => sub {
65 0     0   0 my ($r, $c, $captures, $pattern) = @_;
66             my $uuid
67             = defined $pattern
68             ? ($captures->{$pattern} // $captures->{uuid})
69 0 0 0     0 : ($captures->{uuid} // '');
      0        
70              
71 0 0       0 return 1
72             if $uuid
73             =~ /^[a-f0-9]{8}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{4}-?[a-f0-9]{12}$/i;
74             }
75 1         6 );
76             }
77              
78             $app->routes->add_shortcut(
79             collection => sub {
80 2     2   65 my $r = shift;
81 2         2 my $path = shift;
82 2 50       5 my $options = ref $_[0] eq 'HASH' ? shift : {@_};
83              
84 2   100     5 $options->{element} //= 1;
85 2         3 $options->{route_path} = $path;
86 2         2 $path =~ tr/-/_/;
87             $options->{route_name}
88 2 100       5 = $options->{prefix} ? "$options->{prefix}_$path" : $path;
89              
90             local $options->{collection_method_map} = $options->{collection_method_map}
91 2   33     7 // $conf->{collection_method_map};
92              
93             # generate "/$path" collection route
94             my $controller
95 2 100       3 = $options->{controller} ? "$options->{controller}-$path" : $path;
96 2         12 my $collection = $r->route("/$options->{route_path}")->to("$controller#");
97              
98             # Map HTTP methods to instance methods/mojo actions
99 2         343 while (my ($http_method, $method) = each %{$options->{collection_method_map}}) {
  6         408  
100 4         9 $collection->$http_method->to("#$method")
101             ->name("$options->{route_name}_$method");
102             }
103              
104             return $options->{element}
105 2 100       12 ? $collection->element($options->{route_path}, $options)
106             : $collection;
107             }
108 1         8 );
109              
110             $app->routes->add_shortcut(
111             element => sub {
112 2     2   50 my $r = shift;
113 2         2 my $path = shift;
114 2 50       4 my $options = ref $_[0] eq 'HASH' ? shift : {@_};
115              
116 2   33     10 $options->{over} //= $conf->{over};
117 2   50     19 $options->{placeholder} //= ':';
118 2   33     4 $options->{resource_lookup} //= $conf->{resource_lookup};
119 2         2 $path =~ tr/-/_/;
120             $options->{route_name}
121 2 100       6 = $options->{prefix} ? "$options->{prefix}_$path" : $path;
122              
123             local $options->{element_method_map} = $options->{element_method_map}
124 2   33     6 // $conf->{element_method_map};
125              
126             # generate "/$path/:id" element route with specific placeholder
127             my $element = $r->route("/$options->{placeholder}${path}_id")
128 2         7 ->over($options->{over} => "${path}_id")->name($options->{route_name});
129              
130             # Generate remaining CRUD routes for "/$path/:id", optionally creating a
131             # resource_lookup method for the resource $element.
132             #
133             # This method allows loading an object using the :id in resource_lookup,
134             # and have it accessible via the stash in DELETE, GET, PUT etc. methods
135             # in your controller.
136             my $under
137             = $options->{resource_lookup}
138 2 100       286 ? $element->under->to('#resource_lookup')
139             ->name("$options->{route_name}_resource_lookup")
140             : $element;
141              
142             # Map HTTP methods to instance methods/mojo actions
143 2         86 while (my ($http_method, $method) = each %{$options->{element_method_map}}) {
  10         693  
144 8         17 $under->$http_method->to("#$method")
145             ->name("$options->{route_name}_$method");
146             }
147              
148 2         9 return $element;
149             }
150 1         17 );
151              
152             $app->helper(
153             'restify.current_id' => sub {
154 0     0   0 my $c = shift;
155 0         0 my $name = $c->stash->{controller};
156 0         0 $name =~ s,^.*?\-,,;
157 0   0     0 return $c->match->stack->[-1]->{"${name}_id"} // '';
158             }
159 1         11 );
160              
161             $app->helper(
162             'restify.routes' => sub {
163 2     2   395 my ($self, $r, $routes, $defaults) = @_;
164 2 50       5 return unless $routes;
165              
166             # Allow users to simplify their route creation using an array ref!
167 2 50 33     10 $routes = _arrayref_to_hashref($routes)
168             if ref $routes && ref $routes eq 'ARRAY';
169              
170 2   100     7 $defaults //= {};
171 2   66     5 $defaults->{resource_lookup} //= $conf->{resource_lookup};
172              
173 2         5 while (my ($name, $attrs) = each %$routes) {
174 2         2 my $paths = {};
175 2         5 my $options = {%$defaults};
176              
177 2 50       6 if (ref $attrs eq 'ARRAY') {
    100          
178 0 0       0 $options = {%$options, %{$attrs->[-1]}} if ref $attrs->[-1] eq 'HASH';
  0         0  
179 0 0       0 $paths = shift @$attrs if ref $attrs->[0] eq 'HASH';
180             }
181             elsif (ref $attrs eq 'HASH') {
182 1         1 $paths = $attrs;
183             }
184              
185 2 100       4 if (scalar keys %$paths) {
186 1         2 my $controller = $name;
187 1         2 $controller =~ tr/-/_/;
188 1         15 my $collection = $r->collection($name, {%$options, element => 0});
189             my $under
190             = $options->{resource_lookup}
191             ? $collection->under->to($options->{controller}
192 1 50       5 ? "$options->{controller}-$controller#resource_lookup"
    50          
193             : "$controller#resource_lookup")
194             ->name("${controller}_resource_lookup")
195             : $collection;
196 1         99 my $endpoint
197             = $under->element($name, {%$options, resource_lookup => 0});
198             $options->{controller}
199             = $options->{controller}
200 1 50       5 ? "$options->{controller}-$controller"
201             : $controller;
202             $options->{prefix}
203             = $options->{prefix}
204 1 50       24 ? "$options->{prefix}_$controller"
205             : $controller;
206 1         11 $self->restify->routes($endpoint, $paths, $options);
207             }
208             else {
209 1         5 $r->collection($name, $options);
210             }
211             }
212              
213 2         10 return;
214             }
215 1         15 );
216             }
217              
218             # Aargh eurgh ma bwains!
219             sub _arrayref_to_hashref {
220 17     17   57 my $arrayref = shift;
221 17 100       31 return {} unless defined $arrayref;
222              
223 16         9 my $hashref = {};
224 16         17 for my $path (@$arrayref) {
225 23         17 my $options;
226 23 100 66     34 if (ref $path and ref $path eq 'ARRAY') {
227 1         3 ($path, $options) = @$path;
228             }
229 23         29 my @parts = split '/', $path;
230 23 100       26 if (@parts == 1) {
231 14 100       26 $hashref->{shift @parts} = defined $options ? [undef, $options] : undef;
232             }
233             else {
234 9         9 my $key = shift @parts;
235 9   100     25 $hashref->{$key} //= {};
236             $hashref->{$key}
237 9         7 = {%{$hashref->{$key}}, %{_arrayref_to_hashref([join '/', @parts])}};
  9         10  
  9         19  
238             }
239             }
240              
241 16         38 return $hashref;
242             }
243              
244             1;
245              
246             =encoding utf8
247              
248             =head1 NAME
249              
250             Mojolicious::Plugin::Restify - Route shortcuts & helpers for REST collections
251              
252             =head1 SYNOPSIS
253              
254             # Mojolicious example (Mojolicious::Lite isn't supported)
255             package MyApp;
256             use Mojo::Base 'Mojolicious';
257              
258             sub startup {
259             my $self = shift;
260              
261             # imports the `collection' route shortcut and `restify' helpers
262             $self->plugin('Restify');
263              
264             # add REST collection endpoints manually
265             my $r = $self->routes;
266             my $accounts = $r->collection('accounts'); # /accounts
267             $accounts->collection('invoices'); # /accounts/:accounts_id/invoices
268              
269             # or add the equivalent REST routes with an ARRAYREF (the helper will
270             # create chained routes from the path 'accounts/invoices' so you don't need
271             # to set ['accounts', 'accounts/invoices'])
272             my $r = $self->routes;
273             $self->restify->routes($r, ['accounts/invoices']);
274              
275             # or add the equivalent REST routes with a HASHREF (might be easier to
276             # visualise how collections are chained together)
277             my $r = $self->routes;
278             $self->restify->routes($r, {
279             accounts => {
280             invoices => undef
281             }
282             });
283             }
284              
285             Next create your controller for accounts.
286              
287             # Restify controller depicting the REST actions for the /accounts collection.
288             # (The name of the controller is the Mojo::Util::camelized version of the
289             # collection path.)
290             package MyApp::Controller::Accounts;
291             use Mojo::Base 'Mojolicious::Controller';
292              
293             sub resource_lookup {
294             my $c = shift;
295              
296             # To consistenly get the element's ID relative to the resource_lookup
297             # action, use the helper as shown below. If you need to access an element ID
298             # from a collection further up the chain, you can access it from the stash.
299             #
300             # The naming convention is the name of the collection appended with '_id'.
301             # E.g., $c->stash('accounts_id').
302             my $account = your_lookup_account_resource_func($c->restify->current_id);
303              
304             # By stashing the $account here, it will now be available in the delete,
305             # read, patch, and update actions. This resource_lookup action is optional,
306             # but added to every collection by default to help reduce your code.
307             $c->stash(account => $account);
308              
309             # must return a positive value to continue the dispatch chain
310             return 1 if $account;
311              
312             # inform the end user that this specific resource does not exist
313             $c->reply->not_found and return 0;
314             }
315              
316             sub create { ... }
317              
318             sub delete { ... }
319              
320             sub list { ... }
321              
322             sub read {
323             my $c = shift;
324              
325             # account was placed in the stash in the resource_lookup action
326             $c->render(json => $c->stash('account'));
327             }
328              
329             sub patch { ... }
330              
331             sub update { ... }
332              
333             1;
334              
335             =head1 DESCRIPTION
336              
337             L is a L. It simplifies
338             generating all of the L for a typical REST I
339             endpoint (e.g., C or C) and maps the common HTTP verbs
340             (C, C, C, C, C) to underlying controller class
341             methods.
342              
343             For example, creating a I called C would create the
344             routes as shown below. N.B. The C option in the example below corresponds
345             to the name of a route condition. See L.
346              
347             # The collection route shortcut below creates the following routes, and maps
348             # them to controllers of the camelized route's name.
349             #
350             # Pattern Methods Name Class::Method Name
351             # ------- ------- ---- ------------------
352             # /accounts * accounts
353             # +/ GET "accounts_list" Accounts::list
354             # +/ POST "accounts_create" Accounts::create
355             # +/:accounts_id * "accounts"
356             # +/ * "accounts_resource_lookup" Accounts::resource_lookup
357             # +/ DELETE "accounts_delete" Accounts::delete
358             # +/ GET "accounts_read" Accounts::read
359             # +/ PATCH "accounts_patch" Accounts::patch
360             # +/ PUT "accounts_update" Accounts::update
361              
362             # expects the element id (:accounts_id) for this collection to be a uuid
363             my $route = $r->collection('accounts', over => 'uuid');
364              
365             L tries not to make too many assumptions, but the
366             author's recent experience writing a REST-based API using L has
367             helped shaped this plugin, and might unwittingly express some of his bias.
368              
369             =head1 HELPERS
370              
371             L implements the following helpers.
372              
373             =head2 restify->current_id
374              
375             my $id = $c->restify->current_id;
376              
377             Returns the I id at the current point in the dispatch chain.
378              
379             This is the only way to guarantee the correct I's resource ID in a
380             L I. The C I,
381             which is added by default in both L and L, is
382             added at different positions of the dispatch chain. As such, the router might
383             not have added the value of any placeholders to the
384             L yet.
385              
386             =head2 restify->routes
387              
388             This helper is a wrapper around the L route shortcut. It
389             facilitates creating REST I using either an C or
390             C.
391              
392             It takes a L object, the I to create, and optionally
393             I which are passed to the L route shortcut.
394              
395             # /accounts
396             # /accounts/1234
397             $self->restify->routes($self->routes, ['accounts'], {over => 'int'});
398              
399             # /invoices
400             # /invoices/76be1f53-8363-4ac6-bd83-8b49e07b519c
401             $self->restify->routes($self->routes, ['invoices'], {over => 'uuid'});
402              
403             Maybe you want to chain them.
404              
405             # /accounts
406             # /accounts/1234
407             # /accounts/1234/invoices
408             # /accounts/1234/invoices/76be1f53-8363-4ac6-bd83-8b49e07b519c
409             $self->restify->routes(
410             $self->routes,
411             ['accounts', ['accounts/invoices' => {over => 'uuid'}]],
412             {over => 'int'}
413             );
414              
415             =over
416              
417             =item ARRAYREF
418              
419             Using the elements of the array, invokes L, passing any route-
420             specific options.
421              
422             It will automatically create and chain parent routes if you pass a full path
423             e.g., C<['a/very/long/path']>. This is equivalent to the shell command
424             C.
425              
426             my $restify_routes = [
427             # /area-codes
428             # /area-codes/:area_codes_id/numbers
429             'area-codes/numbers',
430             # /news
431             'news',
432             # /payments
433             ['payments' => {over => 'int'}], # overrides default uuid route condition
434             # /users
435             # /users/:users_id/messages
436             # /users/:users_id/messages/:messages_id/recipients
437             'users/messages/recipients',
438             ];
439              
440             $self->restify->routes($self->routes, $restify_routes, {over => 'uuid'});
441              
442             In its most basic form, C routes are created from a C.
443              
444             # /accounts
445             my $restify_routes = ['accounts'];
446              
447             =item HASHREF
448              
449             Using the key/values of the hash, invokes L, passing any route-
450             specific options.
451              
452             It automatically chains routes to each parent, and progressively builds a
453             namespace as it traverses through each key.
454              
455             N.B., This was implemented before the C version, and is arguably a bit
456             more confusing. It might be dropped in a later version to simplify the API.
457              
458             my $restify_routes = {
459             # /area-codes
460             # /area-codes/:area_codes_id/numbers
461             'area-codes' => {
462             'numbers' => undef
463             },
464             # /news
465             'news' => undef,
466             # /payments
467             'payments' => [undef, {over => 'int'}], # overrides default uuid route condition
468             # /users
469             # /users/:users_id/messages
470             # /users/:users_id/messages/:messages_id/recipients
471             'users' => {
472             'messages' => {
473             'recipients' => undef
474             }
475             },
476             };
477              
478             $self->restify->routes($self->routes, $restify_routes, {over => 'uuid'});
479              
480             =back
481              
482             =head1 METHODS
483              
484             L inherits all methods from L
485             and implements the following new ones.
486              
487             =head2 register
488              
489             $plugin->register(Mojolicious->new);
490              
491             Register plugin in L application.
492              
493             =head1 ROUTE CONDITIONS
494              
495             L implements the following route conditions. These
496             conditions can be used with the C option in the L shortcut.
497              
498             Checks are made for the existence of the C, C and C
499             conditions before adding them. This allows you to replace them with your own
500             conditions of the same name by creating them before registering this plugin.
501              
502             See L to add your own.
503              
504             =head2 int
505              
506             # /numbers/1 # GOOD
507             # /numbers/0 # GOOD
508             # /numbers/one # BAD
509             # /numbers/-1 # BAD
510             # /numbers/0.114 # BAD (the standard :placeholder notation doesn't allow a '.')
511              
512             my $r = $self->routes;
513             $r->collection('numbers', over => 'int');
514              
515             A L route condition (see L) which
516             restricts a route's I's I id to whole positive integers
517             which are C= 0>.
518              
519             =head2 standard
520              
521             my $r = $self->routes;
522             $r->collection('numbers', over => 'standard');
523              
524             A I's I resource ID is captured using
525             L. This route condition
526             allows everything the standard placeholder allows, which is similar to the
527             regular expression C<([^/.]+)>.
528              
529             This is the default I option for a L.
530              
531             =head2 uuid
532              
533             # /uuids/8ebef0d0-d6cf-11e4-8830-0800200c9a66 GOOD
534             # /uuids/8EBEF0D0-D6CF-11E4-8830-0800200C9A66 GOOD
535             # /uuids/8ebef0d0d6cf11e488300800200c9a66 GOOD
536             # /uuids/malformed-uuid BAD
537              
538             my $r = $self->routes;
539             $r->collection('uuids', over => 'uuid');
540              
541             A L route condition (see L) which
542             restricts a route's I's I id to UUIDs only (with or without
543             the separating hyphens).
544              
545             =head1 ROUTE SHORTCUTS
546              
547             L implements the following route shortcuts.
548              
549             =head2 collection
550              
551             my $r = $self->routes;
552             $r->collection('accounts');
553             $r->collection('accounts', collection_method_map => {delete => 'delete_collection'});
554             $r->collection('accounts', controller => 'differentmodule');
555             $r->collection('accounts', element => 0);
556             $r->collection('accounts', element_method_map => {get => 'read'});
557             $r->collection('accounts', over => 'uuid');
558             $r->collection('accounts', placeholder => '*');
559             $r->collection('accounts', prefix => 'v1');
560             $r->collection('accounts', resource_lookup => '0');
561              
562             A L which helps
563             create the most common REST L for a
564             I endpoint and its associated I.
565              
566             A I endpoint (e.g., C) supports I (C) and
567             I (C) actions. The I's I (e.g.,
568             C) supports I (C), I (C),
569             I (C), and I (C) actions.
570              
571             By default, every HTTP request to a I's I is routed through
572             a C I (see L). This
573             helps reduce the process of looking up a I's resource to a single
574             location. See L for an example of its use.
575              
576             =head4 options
577              
578             The following options allow a I to be fine-tuned.
579              
580             =over
581              
582             =item collection_method_map
583              
584             $r->collection(
585             'invoives',
586             {
587             collection_method_map => {
588             get => 'list',
589             post => 'create',
590             # delete => 'delete_collection', # delete all-the-things!
591             # put => 'update_collection' # update all-the-things!
592             }
593             }
594             );
595              
596             The above represents the default HTTP method mappings for C. It's
597             possible to change the mappings globally (when importing the plugin) or per
598             collection (as above).
599              
600             These HTTP method mappings only apply to the C. e.g., C.
601             Please see C if you want to apply different HTTP mappings
602             to an C like C.
603              
604             =item controller
605              
606             # collection doesn't build a namespace for subroutes by default
607             my $accounts = $r->collection('accounts'); # MyApp::Controller::Accounts
608             $accounts->collection('invoices'); # MyApp::Controller::Invoices
609              
610             # collection can build namespaces, but can be difficult to keep track of. Use
611             # the restify helper if namespaces are important to you.
612             #
613             # MyApp::Controller::Accounts
614             my $accounts = $r->collection('accounts');
615             # MyApp::Controller::Accounts::Invoices
616             my $invoices = $accounts->collection('invoices', controller => 'accounts');
617             # MyApp::Controller::Accounts::Invoices::Foo
618             $invoices->collection('foo', controller => 'accounts-invoices');
619              
620             Prepends the controller name (which is automatically generated based on the path
621             name) with this option value if present. Used internally by L to build
622             a perlish namespace from the paths. L does not build a namespace by
623             default.
624              
625             =item element
626              
627             # GET,POST /messages 200
628             # DELETE,GET,PATCH,PUT,UPDATE /messages/1 200
629             $r->collection('messages'); # element routes are created by default
630              
631             # GET,POST /messages 200
632             # DELETE,GET,PATCH,PUT,UPDATE /messages/1 404
633             $r->collection('messages', element => 0);
634              
635             Enables or disables chaining an I to the I. Disabling the
636             element portion of a I means that only the I and I
637             actions will be created.
638              
639             =item element_method_map
640              
641             $r->collection(
642             'invoives',
643             {
644             element_method_map => {
645             'delete' => 'delete',
646             'get' => 'read',
647             'patch' => 'patch',
648             'put' => 'update',
649             }
650             }
651             );
652              
653             The above represents the default HTTP method mappings. It's possible to change
654             the mappings globally (when importing the plugin) or per collection (as above).
655              
656             These HTTP method mappings only apply to the C's C. e.g.,
657             C.
658              
659             =item over
660              
661             $r->collection('invoices', over => 'int');
662             $r->collection('invoices', over => 'standard');
663             $r->collection('accounts', over => 'uuid');
664              
665             Allows a I's I to be restricted to a specific data type
666             using Mojolicious' route conditions. L, L and L are
667             added automatically if they don't already exist.
668              
669             =item placeholder
670              
671             # /versions/:versions_id { versions_id => '123'}
672             # /versions/#versions_id { versions_id => '123.00'}
673             # /versions/*versions_id { versions_id => '123.00/1'}
674              
675             The placeholder is used to capture the I id within a route. It can be
676             one of C, C or C. You might need to
677             adjust the placholder option in certain scenarios, but the C
678             placeholder should suffice for most normal REST endpoints.
679              
680             $r->collection('/messages', placeholder => ':');
681              
682             I are chained to a I using the standard placeholder by
683             default. They match all characters except C and C<.>. See
684             L.
685              
686             $r->collection('/relaxed-messages', placeholder => '#');
687              
688             Placeholders can be relaxed, matching all characters expect C. Useful if you
689             need to capture a domain name within a route. See
690             L.
691              
692             $r->collection('/wildcard-messages', placeholder => '*');
693              
694             Or they can be greedy, matching everything, inclusive of C and C<.>. Useful
695             if you need to capture everything within a route. See
696             L.
697              
698             =item prefix
699              
700             # without a prefix
701             $r->collection('invoices');
702             say $c->url_for('invoices', invoices_id => 1);
703              
704             # with a prefix
705             $r->collection('invoices', prefix => 'v1');
706             say $c->url_for('v1_invoices', invoices_id => 1);
707              
708             Adds a prefix to the automatically generated route
709             L for each I and I
710             I.
711              
712             =item resource_lookup
713              
714             $r->collection('nolookup', resource_lookup => 0);
715              
716             Enables or disables adding a C I to the I of
717             the I.
718              
719             =back
720              
721             =head2 element
722              
723             my $r = $self->routes;
724             my $news = $r->get('/news')->to('foo#news');
725             $news->element('news');
726              
727             A L called internally
728             by L to add the I routes to a I. You shouldn't
729             need to call this shortcut directly.
730              
731             When an element is added to a I's route, the resource ID is captured
732             using a standard placeholder by default.
733              
734             =head1 CREDITS
735              
736             In alphabetical order:
737              
738             =over 2
739              
740             Castaway
741              
742             DragoČ™-Robert Neagu
743              
744             =back
745              
746             =head1 COPYRIGHT AND LICENSE
747              
748             Copyright (C) 2015-2017, Paul Williams.
749              
750             This program is free software, you can redistribute it and/or modify it under
751             the terms of the Artistic License version 2.0.
752              
753             =head1 AUTHOR
754              
755             Paul Williams
756              
757             =head1 SEE ALSO
758              
759             L, L, L.
760              
761             =cut