Merge branch 'runarbu-master'
[catagits/Catalyst-Action-REST.git] / lib / Catalyst / Controller / REST.pm
CommitLineData
256c894f 1package Catalyst::Controller::REST;
f5aa7d45 2
930013e6 3use Moose;
4use namespace::autoclean;
256c894f 5
398c5a1b 6=head1 NAME
7
db8bb647 8Catalyst::Controller::REST - A RESTful controller
398c5a1b 9
10=head1 SYNOPSIS
11
12 package Foo::Controller::Bar;
5cb5f6bb 13 use Moose;
14 use namespace::autoclean;
259c53c7 15
5cb5f6bb 16 BEGIN { extends 'Catalyst::Controller::REST' }
398c5a1b 17
18 sub thing : Local : ActionClass('REST') { }
19
20 # Answer GET requests to "thing"
21 sub thing_GET {
22 my ( $self, $c ) = @_;
db8bb647 23
398c5a1b 24 # Return a 200 OK, with the data in entity
db8bb647 25 # serialized in the body
398c5a1b 26 $self->status_ok(
db8bb647 27 $c,
398c5a1b 28 entity => {
29 some => 'data',
30 foo => 'is real bar-y',
31 },
32 );
33 }
34
35 # Answer PUT requests to "thing"
db8bb647 36 sub thing_PUT {
ace04991 37 my ( $self, $c ) = @_;
38
fcf45ed9 39 $radiohead = $c->req->data->{radiohead};
259c53c7 40
10bcd217 41 $self->status_created(
42 $c,
259c53c7 43 location => $c->req->uri,
10bcd217 44 entity => {
45 radiohead => $radiohead,
46 }
47 );
259c53c7 48 }
398c5a1b 49
50=head1 DESCRIPTION
51
52Catalyst::Controller::REST implements a mechanism for building
53RESTful services in Catalyst. It does this by extending the
db8bb647 54normal Catalyst dispatch mechanism to allow for different
55subroutines to be called based on the HTTP Method requested,
398c5a1b 56while also transparently handling all the serialization/deserialization for
57you.
58
59This is probably best served by an example. In the above
60controller, we have declared a Local Catalyst action on
db8bb647 61"sub thing", and have used the ActionClass('REST').
398c5a1b 62
63Below, we have declared "thing_GET" and "thing_PUT". Any
db8bb647 64GET requests to thing will be dispatched to "thing_GET",
65while any PUT requests will be dispatched to "thing_PUT".
398c5a1b 66
e601adda 67Any unimplemented HTTP methods will be met with a "405 Method Not Allowed"
68response, automatically containing the proper list of available methods. You
69can override this behavior through implementing a custom
db8bb647 70C<thing_not_implemented> method.
e601adda 71
72If you do not provide an OPTIONS handler, we will respond to any OPTIONS
73requests with a "200 OK", populating the Allowed header automatically.
74
75Any data included in C<< $c->stash->{'rest'} >> will be serialized for you.
76The serialization format will be selected based on the content-type
77of the incoming request. It is probably easier to use the L<STATUS HELPERS>,
78which are described below.
398c5a1b 79
10bcd217 80"The HTTP POST, PUT, and OPTIONS methods will all automatically
81L<deserialize|Catalyst::Action::Deserialize> the contents of
259c53c7 82C<< $c->request->body >> into the C<< $c->request->data >> hashref", based on
10bcd217 83the request's C<Content-type> header. A list of understood serialization
84formats is L<below|/AVAILABLE SERIALIZERS>.
398c5a1b 85
e601adda 86If we do not have (or cannot run) a serializer for a given content-type, a 415
db8bb647 87"Unsupported Media Type" error is generated.
398c5a1b 88
89To make your Controller RESTful, simply have it
90
5cb5f6bb 91 BEGIN { extends 'Catalyst::Controller::REST' }
398c5a1b 92
9cd203c9 93=head1 CONFIGURATION
94
95See L<Catalyst::Action::Serialize/CONFIGURATION>. Note that the C<serialize>
96key has been deprecated.
97
398c5a1b 98=head1 SERIALIZATION
99
100Catalyst::Controller::REST will automatically serialize your
e601adda 101responses, and deserialize any POST, PUT or OPTIONS requests. It evaluates
102which serializer to use by mapping a content-type to a Serialization module.
db8bb647 103We select the content-type based on:
e601adda 104
5cb5f6bb 105=over
e601adda 106
107=item B<The Content-Type Header>
108
109If the incoming HTTP Request had a Content-Type header set, we will use it.
110
111=item B<The content-type Query Parameter>
112
113If this is a GET request, you can supply a content-type query parameter.
114
115=item B<Evaluating the Accept Header>
116
117Finally, if the client provided an Accept header, we will evaluate
db8bb647 118it and use the best-ranked choice.
e601adda 119
120=back
121
122=head1 AVAILABLE SERIALIZERS
123
124A given serialization mechanism is only available if you have the underlying
125modules installed. For example, you can't use XML::Simple if it's not already
db8bb647 126installed.
e601adda 127
95318468 128In addition, each serializer has its quirks in terms of what sorts of data
e601adda 129structures it will properly handle. L<Catalyst::Controller::REST> makes
db8bb647 130no attempt to save you from yourself in this regard. :)
e601adda 131
132=over 2
133
95318468 134=item * C<text/x-yaml> => C<YAML::Syck>
e601adda 135
136Returns YAML generated by L<YAML::Syck>.
137
95318468 138=item * C<text/html> => C<YAML::HTML>
e601adda 139
140This uses L<YAML::Syck> and L<URI::Find> to generate YAML with all URLs turned
26b59bcb 141to hyperlinks. Only usable for Serialization.
e601adda 142
95318468 143=item * C<application/json> => C<JSON>
e601adda 144
db8bb647 145Uses L<JSON> to generate JSON output. It is strongly advised to also have
e540a1fa 146L<JSON::XS> installed. The C<text/x-json> content type is supported but is
147deprecated and you will receive warnings in your log.
e601adda 148
838f49dc 149You can also add a hash in your controller config to pass options to the json object.
43e4baa3 150There are two options. C<json_options> are used when decoding incoming JSON, and C<json_options_encode>
151is used when encoding JSON for output.
152
838f49dc 153For instance, to relax permissions when deserializing input, add:
43e4baa3 154
838f49dc 155 __PACKAGE__->config(
156 json_options => { relaxed => 1 }
157 )
158
43e4baa3 159To indent the JSON output so it becomes more human readable, add:
160
161 __PACKAGE__->config(
162 json_options_encode => { indent => 1 }
163 )
164
165
d0d292d4 166=item * C<text/javascript> => C<JSONP>
167
168If a callback=? parameter is passed, this returns javascript in the form of: $callback($serializedJSON);
169
92d78e8f 170Note - this is disabled by default as it can be a security risk if you are unaware.
171
172The usual MIME types for this serialization format are: 'text/javascript', 'application/x-javascript',
173'application/javascript'.
174
95318468 175=item * C<text/x-data-dumper> => C<Data::Serializer>
e601adda 176
177Uses the L<Data::Serializer> module to generate L<Data::Dumper> output.
178
95318468 179=item * C<text/x-data-denter> => C<Data::Serializer>
e601adda 180
181Uses the L<Data::Serializer> module to generate L<Data::Denter> output.
182
95318468 183=item * C<text/x-data-taxi> => C<Data::Serializer>
e601adda 184
185Uses the L<Data::Serializer> module to generate L<Data::Taxi> output.
186
95318468 187=item * C<text/x-config-general> => C<Data::Serializer>
e601adda 188
189Uses the L<Data::Serializer> module to generate L<Config::General> output.
190
95318468 191=item * C<text/x-php-serialization> => C<Data::Serializer>
e601adda 192
193Uses the L<Data::Serializer> module to generate L<PHP::Serialization> output.
194
95318468 195=item * C<text/xml> => C<XML::Simple>
e601adda 196
197Uses L<XML::Simple> to generate XML output. This is probably not suitable
198for any real heavy XML work. Due to L<XML::Simple>s requirement that the data
199you serialize be a HASHREF, we transform outgoing data to be in the form of:
200
201 { data => $yourdata }
202
95318468 203=item * L<View>
9a76221e 204
db8bb647 205Uses a regular Catalyst view. For example, if you wanted to have your
3d8a0645 206C<text/html> and C<text/xml> views rendered by TT, set:
207
208 __PACKAGE__->config(
209 map => {
210 'text/html' => [ 'View', 'TT' ],
211 'text/xml' => [ 'View', 'XML' ],
212 }
5cb5f6bb 213 );
3d8a0645 214
215Your views should have a C<process> method like this:
216
217 sub process {
218 my ( $self, $c, $stash_key ) = @_;
5cb5f6bb 219
3d8a0645 220 my $output;
221 eval {
222 $output = $self->serialize( $c->stash->{$stash_key} );
223 };
224 return $@ if $@;
5cb5f6bb 225
3d8a0645 226 $c->response->body( $output );
227 return 1; # important
228 }
259c53c7 229
3d8a0645 230 sub serialize {
231 my ( $self, $data ) = @_;
5cb5f6bb 232
3d8a0645 233 my $serialized = ... process $data here ...
5cb5f6bb 234
3d8a0645 235 return $serialized;
236 }
9a76221e 237
178f8470 238=item * Callback
239
240For infinite flexibility, you can provide a callback for the
241deserialization/serialization steps.
242
243 __PACKAGE__->config(
244 map => {
245 'text/xml' => [ 'Callback', { deserialize => \&parse_xml, serialize => \&render_xml } ],
246 }
247 );
248
249The C<deserialize> callback is passed a string that is the body of the
250request and is expected to return a scalar value that results from
251the deserialization. The C<serialize> callback is passed the data
252structure that needs to be serialized and must return a string suitable
253for returning in the HTTP response. In addition to receiving the scalar
254to act on, both callbacks are passed the controller object and the context
255(i.e. C<$c>) as the second and third arguments.
256
e601adda 257=back
258
259c53c7 259By default, L<Catalyst::Controller::REST> will return a
95318468 260C<415 Unsupported Media Type> response if an attempt to use an unsupported
261content-type is made. You can ensure that something is always returned by
262setting the C<default> config option:
398c5a1b 263
5cb5f6bb 264 __PACKAGE__->config(default => 'text/x-yaml');
398c5a1b 265
95318468 266would make it always fall back to the serializer plugin defined for
267C<text/x-yaml>.
398c5a1b 268
e601adda 269=head1 CUSTOM SERIALIZERS
270
95318468 271Implementing new Serialization formats is easy! Contributions
259c53c7 272are most welcome! If you would like to implement a custom serializer,
95318468 273you should create two new modules in the L<Catalyst::Action::Serialize>
274and L<Catalyst::Action::Deserialize> namespace. Then assign your new
275class to the content-type's you want, and you're done.
276
259c53c7 277See L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize>
95318468 278for more information.
e601adda 279
398c5a1b 280=head1 STATUS HELPERS
281
e601adda 282Since so much of REST is in using HTTP, we provide these Status Helpers.
283Using them will ensure that you are responding with the proper codes,
284headers, and entities.
285
398c5a1b 286These helpers try and conform to the HTTP 1.1 Specification. You can
db8bb647 287refer to it at: L<http://www.w3.org/Protocols/rfc2616/rfc2616.txt>.
398c5a1b 288These routines are all implemented as regular subroutines, and as
289such require you pass the current context ($c) as the first argument.
290
5cb5f6bb 291=over
398c5a1b 292
293=cut
294
930013e6 295BEGIN { extends 'Catalyst::Controller' }
d4611771 296use Params::Validate qw(SCALAR OBJECT);
256c894f 297
298__PACKAGE__->mk_accessors(qw(serialize));
299
300__PACKAGE__->config(
e540a1fa 301 'stash_key' => 'rest',
302 'map' => {
e540a1fa 303 'text/xml' => 'XML::Simple',
e540a1fa 304 'application/json' => 'JSON',
305 'text/x-json' => 'JSON',
e540a1fa 306 },
c0008fc7 307 'compliance_mode' => 0,
256c894f 308);
309
e540a1fa 310sub begin : ActionClass('Deserialize') { }
5511d1ff 311
0ba73721 312sub end : ActionClass('Serialize') { }
313
398c5a1b 314=item status_ok
315
316Returns a "200 OK" response. Takes an "entity" to serialize.
317
318Example:
319
320 $self->status_ok(
db8bb647 321 $c,
398c5a1b 322 entity => {
323 radiohead => "Is a good band!",
324 }
325 );
326
327=cut
328
329sub status_ok {
330 my $self = shift;
e601adda 331 my $c = shift;
d4611771 332 my %p = Params::Validate::validate( @_, { entity => 1, }, );
398c5a1b 333
334 $c->response->status(200);
e601adda 335 $self->_set_entity( $c, $p{'entity'} );
398c5a1b 336 return 1;
337}
338
339=item status_created
340
341Returns a "201 CREATED" response. Takes an "entity" to serialize,
342and a "location" where the created object can be found.
343
344Example:
345
346 $self->status_created(
db8bb647 347 $c,
259c53c7 348 location => $c->req->uri,
398c5a1b 349 entity => {
350 radiohead => "Is a good band!",
351 }
352 );
353
354In the above example, we use the requested URI as our location.
355This is probably what you want for most PUT requests.
356
357=cut
bb4130f6 358
5511d1ff 359sub status_created {
360 my $self = shift;
e601adda 361 my $c = shift;
d4611771 362 my %p = Params::Validate::validate(
e601adda 363 @_,
5511d1ff 364 {
e601adda 365 location => { type => SCALAR | OBJECT },
366 entity => { optional => 1 },
5511d1ff 367 },
368 );
256c894f 369
5511d1ff 370 $c->response->status(201);
259c53c7 371 $c->response->header( 'Location' => $p{location} );
e601adda 372 $self->_set_entity( $c, $p{'entity'} );
bb4130f6 373 return 1;
374}
375
398c5a1b 376=item status_accepted
377
378Returns a "202 ACCEPTED" response. Takes an "entity" to serialize.
259c53c7 379Also takes optional "location" for queue type scenarios.
398c5a1b 380
381Example:
382
383 $self->status_accepted(
db8bb647 384 $c,
259c53c7 385 location => $c->req->uri,
398c5a1b 386 entity => {
387 status => "queued",
388 }
389 );
390
391=cut
e601adda 392
398c5a1b 393sub status_accepted {
bb4130f6 394 my $self = shift;
e601adda 395 my $c = shift;
259c53c7 396 my %p = Params::Validate::validate(
397 @_,
398 {
399 location => { type => SCALAR | OBJECT, optional => 1 },
400 entity => 1,
401 },
402 );
bb4130f6 403
398c5a1b 404 $c->response->status(202);
259c53c7 405 $c->response->header( 'Location' => $p{location} ) if exists $p{location};
e601adda 406 $self->_set_entity( $c, $p{'entity'} );
bb4130f6 407 return 1;
408}
409
bbf0feae 410=item status_no_content
411
412Returns a "204 NO CONTENT" response.
413
414=cut
415
416sub status_no_content {
417 my $self = shift;
418 my $c = shift;
419 $c->response->status(204);
420 $self->_set_entity( $c, undef );
042656b6 421 return 1;
bbf0feae 422}
423
bdff70a9 424=item status_multiple_choices
425
426Returns a "300 MULTIPLE CHOICES" response. Takes an "entity" to serialize, which should
427provide list of possible locations. Also takes optional "location" for preferred choice.
428
429=cut
430
431sub status_multiple_choices {
432 my $self = shift;
433 my $c = shift;
434 my %p = Params::Validate::validate(
435 @_,
436 {
437 entity => 1,
438 location => { type => SCALAR | OBJECT, optional => 1 },
439 },
440 );
441
bdff70a9 442 $c->response->status(300);
259c53c7 443 $c->response->header( 'Location' => $p{location} ) if exists $p{'location'};
bdff70a9 444 $self->_set_entity( $c, $p{'entity'} );
445 return 1;
446}
447
e52456a4 448=item status_found
449
450Returns a "302 FOUND" response. Takes an "entity" to serialize.
259c53c7 451Also takes optional "location".
e52456a4 452
453=cut
454
455sub status_found {
456 my $self = shift;
457 my $c = shift;
458 my %p = Params::Validate::validate(
459 @_,
460 {
461 entity => 1,
462 location => { type => SCALAR | OBJECT, optional => 1 },
463 },
464 );
465
e52456a4 466 $c->response->status(302);
259c53c7 467 $c->response->header( 'Location' => $p{location} ) if exists $p{'location'};
e52456a4 468 $self->_set_entity( $c, $p{'entity'} );
469 return 1;
470}
471
398c5a1b 472=item status_bad_request
473
474Returns a "400 BAD REQUEST" response. Takes a "message" argument
475as a scalar, which will become the value of "error" in the serialized
476response.
477
478Example:
479
480 $self->status_bad_request(
db8bb647 481 $c,
33e5de96 482 message => "Cannot do what you have asked!",
398c5a1b 483 );
484
485=cut
e601adda 486
cc186a5b 487sub status_bad_request {
488 my $self = shift;
e601adda 489 my $c = shift;
d4611771 490 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
cc186a5b 491
492 $c->response->status(400);
faf5c20b 493 $c->log->debug( "Status Bad Request: " . $p{'message'} ) if $c->debug;
e601adda 494 $self->_set_entity( $c, { error => $p{'message'} } );
cc186a5b 495 return 1;
496}
497
550807bc 498=item status_forbidden
499
500Returns a "403 FORBIDDEN" response. Takes a "message" argument
501as a scalar, which will become the value of "error" in the serialized
502response.
503
504Example:
505
506 $self->status_forbidden(
507 $c,
508 message => "access denied",
509 );
510
511=cut
512
513sub status_forbidden {
514 my $self = shift;
515 my $c = shift;
516 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
517
518 $c->response->status(403);
519 $c->log->debug( "Status Forbidden: " . $p{'message'} ) if $c->debug;
520 $self->_set_entity( $c, { error => $p{'message'} } );
521 return 1;
522}
523
398c5a1b 524=item status_not_found
525
526Returns a "404 NOT FOUND" response. Takes a "message" argument
527as a scalar, which will become the value of "error" in the serialized
528response.
529
530Example:
531
532 $self->status_not_found(
db8bb647 533 $c,
33e5de96 534 message => "Cannot find what you were looking for!",
398c5a1b 535 );
536
537=cut
e601adda 538
bb4130f6 539sub status_not_found {
540 my $self = shift;
e601adda 541 my $c = shift;
d4611771 542 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
bb4130f6 543
544 $c->response->status(404);
faf5c20b 545 $c->log->debug( "Status Not Found: " . $p{'message'} ) if $c->debug;
e601adda 546 $self->_set_entity( $c, { error => $p{'message'} } );
bb4130f6 547 return 1;
548}
549
bbf0feae 550=item gone
551
552Returns a "41O GONE" response. Takes a "message" argument as a scalar,
553which will become the value of "error" in the serialized response.
554
555Example:
556
557 $self->status_gone(
558 $c,
559 message => "The document have been deleted by foo",
560 );
561
562=cut
563
564sub status_gone {
565 my $self = shift;
566 my $c = shift;
567 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
568
569 $c->response->status(410);
570 $c->log->debug( "Status Gone " . $p{'message'} ) if $c->debug;
571 $self->_set_entity( $c, { error => $p{'message'} } );
572 return 1;
573}
574
0aceaa9b 575=item status_see_other
576
577Returns a "303 See Other" response. Takes an optional "entity" to serialize,
578and a "location" where the client should redirect to.
579
580Example:
581
582 $self->status_see_other(
583 $c,
584 location => $some_other_url,
585 entity => {
586 radiohead => "Is a good band!",
587 }
588 );
589
590=cut
591
592sub status_see_other {
593 my $self = shift;
594 my $c = shift;
595 my %p = Params::Validate::validate(
596 @_,
597 {
598 location => { type => SCALAR | OBJECT },
599 entity => { optional => 1 },
600 },
601 );
602
603 $c->response->status(303);
604 $c->response->header( 'Location' => $p{location} );
605 $self->_set_entity( $c, $p{'entity'} );
606 return 1;
607}
608
609=item status_moved
610
611Returns a "301 MOVED" response. Takes an "entity" to serialize, and a
612"location" where the created object can be found.
613
614Example:
615
616 $self->status_moved(
617 $c,
618 location => '/somewhere/else',
619 entity => {
620 radiohead => "Is a good band!",
621 },
622 );
623
624=cut
625
626sub status_moved {
627 my $self = shift;
628 my $c = shift;
629 my %p = Params::Validate::validate(
630 @_,
631 {
632 location => { type => SCALAR | OBJECT },
633 entity => { optional => 1 },
634 },
635 );
636
637 my $location = ref $p{location}
638 ? $p{location}->as_string
639 : $p{location}
640 ;
641
642 $c->response->status(301);
643 $c->response->header( Location => $location );
644 $self->_set_entity($c, $p{entity});
645 return 1;
646}
647
bb4130f6 648sub _set_entity {
e601adda 649 my $self = shift;
650 my $c = shift;
bb4130f6 651 my $entity = shift;
e601adda 652 if ( defined($entity) ) {
faf5c20b 653 $c->stash->{ $self->{'stash_key'} } = $entity;
5511d1ff 654 }
655 return 1;
eccb2137 656}
256c894f 657
398c5a1b 658=back
659
660=head1 MANUAL RESPONSES
661
662If you want to construct your responses yourself, all you need to
663do is put the object you want serialized in $c->stash->{'rest'}.
664
e601adda 665=head1 IMPLEMENTATION DETAILS
666
667This Controller ties together L<Catalyst::Action::REST>,
668L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize>. It should be suitable for most applications. You should be aware that it:
669
670=over 4
671
672=item Configures the Serialization Actions
673
674This class provides a default configuration for Serialization. It is currently:
675
676 __PACKAGE__->config(
95318468 677 'stash_key' => 'rest',
678 'map' => {
679 'text/html' => 'YAML::HTML',
680 'text/xml' => 'XML::Simple',
681 'text/x-yaml' => 'YAML',
682 'application/json' => 'JSON',
683 'text/x-json' => 'JSON',
684 'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ],
685 'text/x-data-denter' => [ 'Data::Serializer', 'Data::Denter' ],
686 'text/x-data-taxi' => [ 'Data::Serializer', 'Data::Taxi' ],
687 'application/x-storable' => [ 'Data::Serializer', 'Storable' ],
688 'application/x-freezethaw' => [ 'Data::Serializer', 'FreezeThaw' ],
689 'text/x-config-general' => [ 'Data::Serializer', 'Config::General' ],
690 'text/x-php-serialization' => [ 'Data::Serializer', 'PHP::Serialization' ],
691 },
e601adda 692 );
693
694You can read the full set of options for this configuration block in
695L<Catalyst::Action::Serialize>.
696
697=item Sets a C<begin> and C<end> method for you
698
699The C<begin> method uses L<Catalyst::Action::Deserialize>. The C<end>
700method uses L<Catalyst::Action::Serialize>. If you want to override
701either behavior, simply implement your own C<begin> and C<end> actions
355d4385 702and forward to another action with the Serialize and/or Deserialize
703action classes:
e601adda 704
10bcd217 705 package Foo::Controller::Monkey;
706 use Moose;
707 use namespace::autoclean;
355d4385 708
10bcd217 709 BEGIN { extends 'Catalyst::Controller::REST' }
e601adda 710
355d4385 711 sub begin : Private {
e601adda 712 my ($self, $c) = @_;
db8bb647 713 ... do things before Deserializing ...
355d4385 714 $c->forward('deserialize');
e601adda 715 ... do things after Deserializing ...
db8bb647 716 }
e601adda 717
355d4385 718 sub deserialize : ActionClass('Deserialize') {}
719
e601adda 720 sub end :Private {
721 my ($self, $c) = @_;
db8bb647 722 ... do things before Serializing ...
355d4385 723 $c->forward('serialize');
e601adda 724 ... do things after Serializing ...
725 }
726
355d4385 727 sub serialize : ActionClass('Serialize') {}
728
8bf1f20e 729If you need to deserialize multipart requests (i.e. REST data in
730one part and file uploads in others) you can do so by using the
731L<Catalyst::Action::DeserializeMultiPart> action class.
732
e540a1fa 733=back
734
e601adda 735=head1 A MILD WARNING
736
737I have code in production using L<Catalyst::Controller::REST>. That said,
738it is still under development, and it's possible that things may change
d6ece98c 739between releases. I promise to not break things unnecessarily. :)
e601adda 740
398c5a1b 741=head1 SEE ALSO
742
743L<Catalyst::Action::REST>, L<Catalyst::Action::Serialize>,
744L<Catalyst::Action::Deserialize>
745
746For help with REST in general:
747
748The HTTP 1.1 Spec is required reading. http://www.w3.org/Protocols/rfc2616/rfc2616.txt
749
750Wikipedia! http://en.wikipedia.org/wiki/Representational_State_Transfer
751
752The REST Wiki: http://rest.blueoxen.net/cgi-bin/wiki.pl?FrontPage
753
5cb5f6bb 754=head1 AUTHORS
e540a1fa 755
5cb5f6bb 756See L<Catalyst::Action::REST> for authors.
e540a1fa 757
398c5a1b 758=head1 LICENSE
759
760You may distribute this code under the same terms as Perl itself.
761
762=cut
763
24748286 764__PACKAGE__->meta->make_immutable;
765
256c894f 7661;