File Coverage

lib/Catalyst/Action/REST.pm
Criterion Covered Total %
statement 77 77 100.0
branch 13 14 92.8
condition 4 5 80.0
subroutine 16 16 100.0
pod 2 3 66.6
total 112 115 97.3


line stmt bran cond sub pod time code
1             package Catalyst::Action::REST;
2             $Catalyst::Action::REST::VERSION = '1.19';
3 6     6   205308 use utf8;
  6         14  
  6         54  
4 6     6   197 use Moose;
  6         8  
  6         43  
5 6     6   34793 use namespace::autoclean;
  6         13  
  6         59  
6              
7             extends 'Catalyst::Action';
8 6     6   4985 use Class::Inspector;
  6         16509  
  6         326  
9 6     6   926 use Catalyst::Request::REST;
  6         11  
  6         177  
10 6     6   39 use Catalyst::Controller::REST;
  6         9  
  6         326  
11              
12 6     6   5654 BEGIN { require 5.008001; }
13              
14             sub BUILDARGS {
15 54     54 1 83 my $class = shift;
16 54         88 my $config = shift;
17 54         337 Catalyst::Request::REST->_insert_self_into( $config->{class} );
18 54         1757 return $class->SUPER::BUILDARGS($config, @_);
19             }
20              
21             =encoding utf-8
22              
23             =head1 NAME
24              
25             Catalyst::Action::REST - Automated REST Method Dispatching
26              
27             =head1 SYNOPSIS
28              
29             sub foo :Local :ActionClass('REST') {
30             ... do setup for HTTP method specific handlers ...
31             }
32              
33             sub foo_GET {
34             ... do something for GET requests ...
35             }
36              
37             # alternatively use an Action
38             sub foo_PUT : Action {
39             ... do something for PUT requests ...
40             }
41              
42             =head1 DESCRIPTION
43              
44             This Action handles doing automatic method dispatching for REST requests. It
45             takes a normal Catalyst action, and changes the dispatch to append an
46             underscore and method name. First it will try dispatching to an action with
47             the generated name, and failing that it will try to dispatch to a regular
48             method.
49              
50             For example, in the synopsis above, calling GET on "/foo" would result in
51             the foo_GET method being dispatched.
52              
53             If a method is requested that is not implemented, this action will
54             return a status 405 (Method Not Found). It will populate the "Allow" header
55             with the list of implemented request methods. You can override this behavior
56             by implementing a custom 405 handler like so:
57              
58             sub foo_not_implemented {
59             ... handle not implemented methods ...
60             }
61              
62             If you do not provide an _OPTIONS subroutine, we will automatically respond
63             with a 200 OK. The "Allow" header will be populated with the list of
64             implemented request methods. If you do not provide an _HEAD either, we will
65             auto dispatch to the _GET one in case it exists.
66              
67             It is likely that you really want to look at L<Catalyst::Controller::REST>,
68             which brings this class together with automatic Serialization of requests
69             and responses.
70              
71             When you use this module, it adds the L<Catalyst::TraitFor::Request::REST>
72             role to your request class.
73              
74             =head1 METHODS
75              
76             =over 4
77              
78             =item dispatch
79              
80             This method overrides the default dispatch mechanism to the re-dispatching
81             mechanism described above.
82              
83             =cut
84              
85             sub dispatch {
86 27     27 1 146446 my $self = shift;
87 27         54 my $c = shift;
88              
89 27         637 my $rest_method = $self->name . "_" . uc( $c->request->method );
90              
91 27         252 return $self->_dispatch_rest_method( $c, $rest_method );
92             }
93              
94             sub _dispatch_rest_method {
95 31     31   44 my $self = shift;
96 31         41 my $c = shift;
97 31         38 my $rest_method = shift;
98 31         680 my $req = $c->request;
99              
100 31         855 my $controller = $c->component( $self->class );
101              
102 31         1084 my ($code, $name);
103              
104             # Execute normal 'foo' action.
105 31         769 $c->execute( $self->class, $self, @{ $req->args } );
  31         192  
106              
107             # Common case, for foo_GET etc
108 31 100       11917 if ( $code = $controller->action_for($rest_method) ) {
    100          
109 11         2009 return $c->forward( $code, $req->args ); # Forward to foo_GET if it's an action
110             }
111             elsif ($code = $controller->can($rest_method)) {
112 11         2116 $name = $rest_method; # Stash name and code to run 'foo_GET' like an action below.
113             }
114              
115             # Generic handling for foo_*
116 20 100       1698 if (!$code) {
117             my $code_action = {
118             OPTIONS => sub {
119 3     3   7 $name = $rest_method;
120 3         16 $code = sub { $self->_return_options($self->name, @_) };
  3         639  
121             },
122             HEAD => sub {
123 3     3   13 $rest_method =~ s{_HEAD$}{_GET}i;
124 3         18 $self->_dispatch_rest_method($c, $rest_method);
125             },
126             default => sub {
127             # Otherwise, not implemented.
128 3     3   125 $name = $self->name . "_not_implemented";
129             $code = $controller->can($name) # User method
130             # Generic not implemented
131 3   100     51 || sub { $self->_return_not_implemented($self->name, @_) };
132             },
133 9         131 };
134 9         227 my ( $http_method, $action_name ) = ( $rest_method, $self->name );
135 9         182 $http_method =~ s{\Q$action_name\E\_}{};
136 9   66     51 my $respond = ($code_action->{$http_method}
137             || $code_action->{'default'})->();
138 9 100       2183 return $respond unless $name;
139             }
140              
141             # localise stuff so we can dispatch the action 'as normal, but get
142             # different stats shown, and different code run.
143             # Also get the full path for the action, and make it look like a forward
144 17         52 local $self->{code} = $code;
145 17         476 my @name = split m{/}, $self->reverse;
146 17         152 $name[-1] = $name;
147 17         70 local $self->{reverse} = "-> " . join('/', @name);
148              
149 17         427 $c->execute( $self->class, $self, @{ $req->args } );
  17         102  
150             }
151              
152             sub get_allowed_methods {
153 5     5 0 10 my ( $self, $controller, $c, $name ) = @_;
154 5 50       16 my $class = ref($controller) ? ref($controller) : $controller;
155 585 100       5838 my $methods = {
156 5         40 map { /^$name\_(.+)$/ ? ( $1 => 1 ) : () }
157 5         6 @{ Class::Inspector->methods($class) }
158             };
159 5 100       42 $methods->{'HEAD'} = 1 if $methods->{'GET'};
160 5         14 delete $methods->{'not_implemented'};
161 5         28 return sort keys %$methods;
162             };
163              
164             sub _return_options {
165 3     3   22 my ( $self, $method_name, $controller, $c) = @_;
166 3         11 my @allowed = $self->get_allowed_methods($controller, $c, $method_name);
167 3         93 $c->response->content_type('text/plain');
168 3         567 $c->response->status(200);
169 3         247 $c->response->header( 'Allow' => \@allowed );
170 3         510 $c->response->body(q{});
171             }
172              
173             sub _return_not_implemented {
174 2     2   13 my ( $self, $method_name, $controller, $c ) = @_;
175              
176 2         7 my @allowed = $self->get_allowed_methods($controller, $c, $method_name);
177 2         60 $c->response->content_type('text/plain');
178 2         359 $c->response->status(405);
179 2         170 $c->response->header( 'Allow' => \@allowed );
180 2         321 $c->response->body( "Method "
181             . $c->request->method
182             . " not implemented for "
183             . $c->uri_for( $method_name ) );
184             }
185              
186             __PACKAGE__->meta->make_immutable;
187              
188             1;
189              
190             =back
191              
192             =head1 SEE ALSO
193              
194             You likely want to look at L<Catalyst::Controller::REST>, which implements a
195             sensible set of defaults for a controller doing REST.
196              
197             This class automatically adds the L<Catalyst::TraitFor::Request::REST> role to
198             your request class. If you're writing a web application which provides RESTful
199             responses and still needs to accommodate web browsers, you may prefer to use
200             L<Catalyst::TraitFor::Request::REST::ForBrowsers> instead.
201              
202             L<Catalyst::Action::Serialize>, L<Catalyst::Action::Deserialize>
203              
204             =head1 TROUBLESHOOTING
205              
206             =over 4
207              
208             =item Q: I'm getting a "415 Unsupported Media Type" error. What gives?!
209              
210             A: Most likely, you haven't set Content-type equal to "application/json", or
211             one of the accepted return formats. You can do this by setting it in your query
212             accepted return formats. You can do this by setting it in your query string
213             thusly: C<< ?content-type=application%2Fjson (where %2F == / uri escaped). >>
214              
215             B<NOTE> Apache will refuse %2F unless configured otherwise.
216             Make sure C<AllowEncodedSlashes On> is in your httpd.conf file in order
217             for this to run smoothly.
218              
219             =back
220              
221             =head1 AUTHOR
222              
223             Adam Jacob E<lt>adam@stalecoffee.orgE<gt>, with lots of help from mst and jrockway
224              
225             Marchex, Inc. paid me while I developed this module. (L<http://www.marchex.com>)
226              
227             =head1 CONTRIBUTORS
228              
229             Tomas Doran (t0m) E<lt>bobtfish@bobtfish.netE<gt>
230              
231             John Goulah
232              
233             Christopher Laco
234              
235             Daisuke Maki E<lt>daisuke@endeworks.jpE<gt>
236              
237             Hans Dieter Pearcey
238              
239             Brian Phillips E<lt>bphillips@cpan.orgE<gt>
240              
241             Dave Rolsky E<lt>autarch@urth.orgE<gt>
242              
243             Luke Saunders
244              
245             Arthur Axel "fREW" Schmidt E<lt>frioux@gmail.comE<gt>
246              
247             J. Shirley E<lt>jshirley@gmail.comE<gt>
248              
249             Gavin Henry E<lt>ghenry@surevoip.co.ukE<gt>
250              
251             Gerv http://www.gerv.net/
252              
253             Colin Newell <colin@opusvl.com>
254              
255             Wallace Reis E<lt>wreis@cpan.orgE<gt>
256              
257             André Walker (andrewalker) <andre@cpan.org>
258              
259             =head1 COPYRIGHT
260              
261             Copyright (c) 2006-2015 the above named AUTHOR and CONTRIBUTORS
262              
263             =head1 LICENSE
264              
265             You may distribute this code under the same terms as Perl itself.
266              
267             =cut
268