File Coverage

lib/Mojolicious/Plugin/RESTRoutes.pm
Criterion Covered Total %
statement 41 42 97.6
branch 9 12 75.0
condition 6 8 75.0
subroutine 6 6 100.0
pod 1 1 100.0
total 63 69 91.3


line stmt bran cond sub pod time code
1 1     1   196711 use Modern::Perl; # strict, warnings etc.;
  1         1432  
  1         4  
2             package Mojolicious::Plugin::RESTRoutes;
3             # ABSTRACT: routing helper for RESTful operations
4             # VERSION
5             $Mojolicious::Plugin::RESTRoutes::VERSION = '1.0.0';
6 1     1   100 use Mojo::Base 'Mojolicious::Plugin';
  1         1  
  1         5  
7              
8             #pod =encoding utf8
9             #pod
10             #pod =head1 DESCRIPTION
11             #pod
12             #pod This Mojolicious plugin adds a routing helper for
13             #pod Lful
14             #pod L
15             #pod operations via HTTP to the app.
16             #pod
17             #pod The routes are intended, but not restricted to be used by AJAX applications.
18             #pod
19             #pod =cut
20              
21 1     1   1641 use Lingua::EN::Inflect qw/PL/;
  1         17035  
  1         93  
22 1     1   5 use Mojo::Util qw( camelize );
  1         1  
  1         469  
23              
24             #pod =method register
25             #pod
26             #pod Adds the routing helper (called by Mojolicious).
27             #pod
28             #pod =cut
29             sub register {
30 1     1 1 8949 my ($self, $app) = @_;
31              
32             #pod =mojo_short rest_routes
33             #pod
34             #pod Can be used to easily generate the needed RESTful routes for a resource.
35             #pod
36             #pod my $r = $self->routes;
37             #pod my $userroute = $r->rest_routes(name => 'user');
38             #pod
39             #pod # Installs the following routes (given that $r->namespaces == ['My::Mojo']):
40             #pod # GET /users --> My::Mojo::User::rest_list()
41             #pod # POST /users --> My::Mojo::User::rest_create()
42             #pod # GET /users/:userid --> My::Mojo::User::rest_show()
43             #pod # PUT /users/:userid --> My::Mojo::User::rest_update()
44             #pod # DELETE /users/:userid --> My::Mojo::User::rest_remove()
45             #pod
46             #pod I: the english plural form of the given C attribute will be
47             #pod used in the route, i.e. "users" instead of "user". If you want to specify
48             #pod another string, see parameter C below.
49             #pod
50             #pod You can also chain C:
51             #pod
52             #pod $userroute->rest_routes(name => 'hat', readonly => 1);
53             #pod
54             #pod # Installs the following additional routes:
55             #pod # GET /users/:userid/hats --> My::Mojo::Hat::rest_list()
56             #pod # GET /users/:userid/hats/:hatid --> My::Mojo::Hat::rest_show()
57             #pod
58             #pod The target controller has to implement the following methods:
59             #pod
60             #pod =for :list
61             #pod * C
62             #pod * C
63             #pod * C
64             #pod * C
65             #pod * C
66             #pod
67             #pod B
68             #pod
69             #pod =over
70             #pod
71             #pod =item name
72             #pod
73             #pod The name of the resource, e.g. a "user", a "book" etc. This name will be used to
74             #pod build the route URL as well as the controller name (see example above).
75             #pod
76             #pod =item readonly (optional)
77             #pod
78             #pod If set to 1, no create/update/delete routes will be created
79             #pod
80             #pod =item controller (optional)
81             #pod
82             #pod Default behaviour is to use the resource name to build the CamelCase controller
83             #pod name (this is done by L). You can change this by
84             #pod directly specifying the controller's name via the I attribute.
85             #pod
86             #pod Note that you have to give the real controller class name (i.e. CamelCased or
87             #pod whatever you class name looks like) including the full namespace.
88             #pod
89             #pod $r->rest_routes(name => 'user', controller => 'My::Mojo::Person');
90             #pod
91             #pod # Installs the following routes:
92             #pod # GET /users --> My::Mojo::Person::rest_list()
93             #pod # ...
94             #pod
95             #pod =item route (optional)
96             #pod
97             #pod Specify a name for the route, i.e. prevent automatic usage of english plural
98             #pod form of the C parameter as the route component.
99             #pod
100             #pod $r->rest_routes(name => 'angst', route => 'aengste');
101             #pod
102             #pod # Installs the following routes (given that $r->namespaces == ['My::Mojo']):
103             #pod # GET /aengste --> My::Mojo::Angst::rest_list()
104             #pod
105             #pod =back
106             #pod
107             #pod B
108             #pod
109             #pod There are two ways to retrieve the IDs given by the client in your C,
110             #pod C and C methods.
111             #pod
112             #pod Example request: C
113             #pod
114             #pod 1. New way: the stash entry C holds a hash with all ids:
115             #pod
116             #pod package My::Mojo::Hats;
117             #pod use Mojo::Base 'Mojolicious::Controller';
118             #pod
119             #pod sub rest_show {
120             #pod use Data::Dump qw(dump);
121             #pod print dump($self->stash('fm.ids'));
122             #pod
123             #pod # { user => 5, hat => 'no9' }
124             #pod }
125             #pod
126             #pod 2. Old way: for each resource there will be a parameter C<***id>, e.g.:
127             #pod
128             #pod package My::Mojo::Hat;
129             #pod use Mojo::Base 'Mojolicious::Controller';
130             #pod
131             #pod sub rest_show {
132             #pod my ($self) = @_;
133             #pod my $user = $self->param('userid');
134             #pod my $hat = $self->param('hatid');
135             #pod return $self->render(text => "$userid, $hatid");
136             #pod
137             #pod # text: "5, no9"
138             #pod }
139             #pod
140             #pod Furthermore, the parameter C holds the name of the last ID in the route:
141             #pod
142             #pod package My::Mojo::Hat;
143             #pod use Mojo::Base 'Mojolicious::Controller';
144             #pod
145             #pod sub rest_show {
146             #pod my $p_name = $self->param('idname');
147             #pod my $id = $self->param($p_name);
148             #pod return $self->render(text => sprintf("%s = %s", $p_name, $id || ''));
149             #pod
150             #pod # text: "hatid = 5"
151             #pod }
152             #pod
153             #pod =cut
154             # For the following TODOs also see http://pastebin.com/R9zXrtCg
155             # TODO Add GET /users/new --> rest_create_user_form
156             # TODO Add GET /users/:userid/edit --> rest_edit_user_form
157             # TODO Add GET /users/:userid/delete --> rest_delete_user_form
158             # TODO Add GET /users/search --> rest_search_user_form
159             # TODO Add PUT /users/search/:term --> rest_search_user_form (submit/execution)
160             $app->routes->add_shortcut(
161             rest_routes => sub {
162 4     4   333 my $r = shift;
163 4 50       16 my $params = { @_ ? (ref $_[0] ? %{ $_[0] } : @_) : () };
  0 50       0  
164              
165 4 50       10 my $name = $params->{name} or die "Parameter 'name' missing";
166 4   100     10 my $readonly = $params->{readonly} || 0;
167 4   66     11 my $controller = $params->{controller} || $name;
168 4   66     15 my $route_part = $params->{route} || PL($name, 10); # build english plural form
169              
170 4         3288 $app->log->info("Creating REST routes for resource '$name' (controller: ".camelize($controller).")");
171              
172             #
173             # Generate "/$name" route, handled by controller $name
174             #
175 4         211 my $resource = $r->route("/$route_part")->to(controller => $controller);
176              
177             # GET requests - lists the collection of this resource
178 4         472 $resource->get->to('#rest_list')->name("list_$route_part");
179 4         378 $app->log->debug(" GET ".$r->to_string."/$route_part (rest_list)");
180              
181 4 100       147 if (!$readonly) {
182             # POST requests - creates a new resource
183 1         3 $resource->post->to('#rest_create')->name("create_$name");
184 1         107 $app->log->debug(" POST ".$r->to_string."/$route_part (rest_create)");
185             };
186              
187             #
188             # Generate "/$name/:id" route, also handled by controller $name
189             #
190              
191             # resource routes might be chained, so we need to define an
192             # individual id and pass its name to the controller (idname)
193             $resource = $r
194             ->under("/$route_part/:${name}id" => sub {
195 10         166209 my ($c) = @_;
196 10         24 $c->app->log->debug(sprintf("Feeding ID into stash: \$c->stash('fm.ids')->{'%s'} = %s", $name, $c->param("${name}id")));
197 10 100       321 $c->stash('fm.ids' => {}) unless $c->stash('fm.ids');
198 10         168 $c->stash('fm.ids')->{$name} = $c->param("${name}id");
199 10         182 return 1;
200             })
201 4         56 ->to(controller => $controller, idname => "${name}id");
202              
203             # GET requests - lists a single resource
204 4         672 $resource->get->to("#rest_show")->name("show_$name");
205 4         364 $app->log->debug(" GET ".$r->to_string."/$route_part/:${name}id (rest_show)");
206              
207 4 100       134 if (!$readonly) {
208             # DELETE requests - deletes a resource
209 1         4 $resource->delete->to('#rest_remove')->name("delete_$name");
210 1         93 $app->log->debug(" DELETE ".$r->to_string."/$route_part/:${name}id (rest_delete)");
211              
212             # PUT requests - updates a resource
213 1         36 $resource->put->to('#rest_update')->name("update_$name");
214 1         90 $app->log->debug(" PUT ".$r->to_string."/$route_part/:${name}id (rest_update)");
215             }
216              
217             # return "/$name/:id" route so that potential child routes make sense
218 4         43 return $resource;
219             }
220 1         4 );
221             }
222              
223             1;
224              
225             __END__