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