File Coverage

blib/lib/Mojolicious/Plugin/Restify.pm
Criterion Covered Total %
statement 88 102 86.2
branch 36 58 62.0
condition 19 44 43.1
subroutine 8 11 72.7
pod 1 1 100.0
total 152 216 70.3


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