New option json_options_encode to set JSON options for output
[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 },
256c894f 307);
308
e540a1fa 309sub begin : ActionClass('Deserialize') { }
5511d1ff 310
0ba73721 311sub end : ActionClass('Serialize') { }
312
398c5a1b 313=item status_ok
314
315Returns a "200 OK" response. Takes an "entity" to serialize.
316
317Example:
318
319 $self->status_ok(
db8bb647 320 $c,
398c5a1b 321 entity => {
322 radiohead => "Is a good band!",
323 }
324 );
325
326=cut
327
328sub status_ok {
329 my $self = shift;
e601adda 330 my $c = shift;
d4611771 331 my %p = Params::Validate::validate( @_, { entity => 1, }, );
398c5a1b 332
333 $c->response->status(200);
e601adda 334 $self->_set_entity( $c, $p{'entity'} );
398c5a1b 335 return 1;
336}
337
338=item status_created
339
340Returns a "201 CREATED" response. Takes an "entity" to serialize,
341and a "location" where the created object can be found.
342
343Example:
344
345 $self->status_created(
db8bb647 346 $c,
259c53c7 347 location => $c->req->uri,
398c5a1b 348 entity => {
349 radiohead => "Is a good band!",
350 }
351 );
352
353In the above example, we use the requested URI as our location.
354This is probably what you want for most PUT requests.
355
356=cut
bb4130f6 357
5511d1ff 358sub status_created {
359 my $self = shift;
e601adda 360 my $c = shift;
d4611771 361 my %p = Params::Validate::validate(
e601adda 362 @_,
5511d1ff 363 {
e601adda 364 location => { type => SCALAR | OBJECT },
365 entity => { optional => 1 },
5511d1ff 366 },
367 );
256c894f 368
5511d1ff 369 $c->response->status(201);
259c53c7 370 $c->response->header( 'Location' => $p{location} );
e601adda 371 $self->_set_entity( $c, $p{'entity'} );
bb4130f6 372 return 1;
373}
374
398c5a1b 375=item status_accepted
376
377Returns a "202 ACCEPTED" response. Takes an "entity" to serialize.
259c53c7 378Also takes optional "location" for queue type scenarios.
398c5a1b 379
380Example:
381
382 $self->status_accepted(
db8bb647 383 $c,
259c53c7 384 location => $c->req->uri,
398c5a1b 385 entity => {
386 status => "queued",
387 }
388 );
389
390=cut
e601adda 391
398c5a1b 392sub status_accepted {
bb4130f6 393 my $self = shift;
e601adda 394 my $c = shift;
259c53c7 395 my %p = Params::Validate::validate(
396 @_,
397 {
398 location => { type => SCALAR | OBJECT, optional => 1 },
399 entity => 1,
400 },
401 );
bb4130f6 402
398c5a1b 403 $c->response->status(202);
259c53c7 404 $c->response->header( 'Location' => $p{location} ) if exists $p{location};
e601adda 405 $self->_set_entity( $c, $p{'entity'} );
bb4130f6 406 return 1;
407}
408
bbf0feae 409=item status_no_content
410
411Returns a "204 NO CONTENT" response.
412
413=cut
414
415sub status_no_content {
416 my $self = shift;
417 my $c = shift;
418 $c->response->status(204);
419 $self->_set_entity( $c, undef );
042656b6 420 return 1;
bbf0feae 421}
422
bdff70a9 423=item status_multiple_choices
424
425Returns a "300 MULTIPLE CHOICES" response. Takes an "entity" to serialize, which should
426provide list of possible locations. Also takes optional "location" for preferred choice.
427
428=cut
429
430sub status_multiple_choices {
431 my $self = shift;
432 my $c = shift;
433 my %p = Params::Validate::validate(
434 @_,
435 {
436 entity => 1,
437 location => { type => SCALAR | OBJECT, optional => 1 },
438 },
439 );
440
bdff70a9 441 $c->response->status(300);
259c53c7 442 $c->response->header( 'Location' => $p{location} ) if exists $p{'location'};
bdff70a9 443 $self->_set_entity( $c, $p{'entity'} );
444 return 1;
445}
446
e52456a4 447=item status_found
448
449Returns a "302 FOUND" response. Takes an "entity" to serialize.
259c53c7 450Also takes optional "location".
e52456a4 451
452=cut
453
454sub status_found {
455 my $self = shift;
456 my $c = shift;
457 my %p = Params::Validate::validate(
458 @_,
459 {
460 entity => 1,
461 location => { type => SCALAR | OBJECT, optional => 1 },
462 },
463 );
464
e52456a4 465 $c->response->status(302);
259c53c7 466 $c->response->header( 'Location' => $p{location} ) if exists $p{'location'};
e52456a4 467 $self->_set_entity( $c, $p{'entity'} );
468 return 1;
469}
470
398c5a1b 471=item status_bad_request
472
473Returns a "400 BAD REQUEST" response. Takes a "message" argument
474as a scalar, which will become the value of "error" in the serialized
475response.
476
477Example:
478
479 $self->status_bad_request(
db8bb647 480 $c,
33e5de96 481 message => "Cannot do what you have asked!",
398c5a1b 482 );
483
484=cut
e601adda 485
cc186a5b 486sub status_bad_request {
487 my $self = shift;
e601adda 488 my $c = shift;
d4611771 489 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
cc186a5b 490
491 $c->response->status(400);
faf5c20b 492 $c->log->debug( "Status Bad Request: " . $p{'message'} ) if $c->debug;
e601adda 493 $self->_set_entity( $c, { error => $p{'message'} } );
cc186a5b 494 return 1;
495}
496
550807bc 497=item status_forbidden
498
499Returns a "403 FORBIDDEN" response. Takes a "message" argument
500as a scalar, which will become the value of "error" in the serialized
501response.
502
503Example:
504
505 $self->status_forbidden(
506 $c,
507 message => "access denied",
508 );
509
510=cut
511
512sub status_forbidden {
513 my $self = shift;
514 my $c = shift;
515 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
516
517 $c->response->status(403);
518 $c->log->debug( "Status Forbidden: " . $p{'message'} ) if $c->debug;
519 $self->_set_entity( $c, { error => $p{'message'} } );
520 return 1;
521}
522
398c5a1b 523=item status_not_found
524
525Returns a "404 NOT FOUND" response. Takes a "message" argument
526as a scalar, which will become the value of "error" in the serialized
527response.
528
529Example:
530
531 $self->status_not_found(
db8bb647 532 $c,
33e5de96 533 message => "Cannot find what you were looking for!",
398c5a1b 534 );
535
536=cut
e601adda 537
bb4130f6 538sub status_not_found {
539 my $self = shift;
e601adda 540 my $c = shift;
d4611771 541 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
bb4130f6 542
543 $c->response->status(404);
faf5c20b 544 $c->log->debug( "Status Not Found: " . $p{'message'} ) if $c->debug;
e601adda 545 $self->_set_entity( $c, { error => $p{'message'} } );
bb4130f6 546 return 1;
547}
548
bbf0feae 549=item gone
550
551Returns a "41O GONE" response. Takes a "message" argument as a scalar,
552which will become the value of "error" in the serialized response.
553
554Example:
555
556 $self->status_gone(
557 $c,
558 message => "The document have been deleted by foo",
559 );
560
561=cut
562
563sub status_gone {
564 my $self = shift;
565 my $c = shift;
566 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
567
568 $c->response->status(410);
569 $c->log->debug( "Status Gone " . $p{'message'} ) if $c->debug;
570 $self->_set_entity( $c, { error => $p{'message'} } );
571 return 1;
572}
573
0aceaa9b 574=item status_see_other
575
576Returns a "303 See Other" response. Takes an optional "entity" to serialize,
577and a "location" where the client should redirect to.
578
579Example:
580
581 $self->status_see_other(
582 $c,
583 location => $some_other_url,
584 entity => {
585 radiohead => "Is a good band!",
586 }
587 );
588
589=cut
590
591sub status_see_other {
592 my $self = shift;
593 my $c = shift;
594 my %p = Params::Validate::validate(
595 @_,
596 {
597 location => { type => SCALAR | OBJECT },
598 entity => { optional => 1 },
599 },
600 );
601
602 $c->response->status(303);
603 $c->response->header( 'Location' => $p{location} );
604 $self->_set_entity( $c, $p{'entity'} );
605 return 1;
606}
607
608=item status_moved
609
610Returns a "301 MOVED" response. Takes an "entity" to serialize, and a
611"location" where the created object can be found.
612
613Example:
614
615 $self->status_moved(
616 $c,
617 location => '/somewhere/else',
618 entity => {
619 radiohead => "Is a good band!",
620 },
621 );
622
623=cut
624
625sub status_moved {
626 my $self = shift;
627 my $c = shift;
628 my %p = Params::Validate::validate(
629 @_,
630 {
631 location => { type => SCALAR | OBJECT },
632 entity => { optional => 1 },
633 },
634 );
635
636 my $location = ref $p{location}
637 ? $p{location}->as_string
638 : $p{location}
639 ;
640
641 $c->response->status(301);
642 $c->response->header( Location => $location );
643 $self->_set_entity($c, $p{entity});
644 return 1;
645}
646
bb4130f6 647sub _set_entity {
e601adda 648 my $self = shift;
649 my $c = shift;
bb4130f6 650 my $entity = shift;
e601adda 651 if ( defined($entity) ) {
faf5c20b 652 $c->stash->{ $self->{'stash_key'} } = $entity;
5511d1ff 653 }
654 return 1;
eccb2137 655}
256c894f 656
398c5a1b 657=back
658
659=head1 MANUAL RESPONSES
660
661If you want to construct your responses yourself, all you need to
662do is put the object you want serialized in $c->stash->{'rest'}.
663
e601adda 664=head1 IMPLEMENTATION DETAILS
665
666This Controller ties together L<Catalyst::Action::REST>,
667L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize>. It should be suitable for most applications. You should be aware that it:
668
669=over 4
670
671=item Configures the Serialization Actions
672
673This class provides a default configuration for Serialization. It is currently:
674
675 __PACKAGE__->config(
95318468 676 'stash_key' => 'rest',
677 'map' => {
678 'text/html' => 'YAML::HTML',
679 'text/xml' => 'XML::Simple',
680 'text/x-yaml' => 'YAML',
681 'application/json' => 'JSON',
682 'text/x-json' => 'JSON',
683 'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ],
684 'text/x-data-denter' => [ 'Data::Serializer', 'Data::Denter' ],
685 'text/x-data-taxi' => [ 'Data::Serializer', 'Data::Taxi' ],
686 'application/x-storable' => [ 'Data::Serializer', 'Storable' ],
687 'application/x-freezethaw' => [ 'Data::Serializer', 'FreezeThaw' ],
688 'text/x-config-general' => [ 'Data::Serializer', 'Config::General' ],
689 'text/x-php-serialization' => [ 'Data::Serializer', 'PHP::Serialization' ],
690 },
e601adda 691 );
692
693You can read the full set of options for this configuration block in
694L<Catalyst::Action::Serialize>.
695
696=item Sets a C<begin> and C<end> method for you
697
698The C<begin> method uses L<Catalyst::Action::Deserialize>. The C<end>
699method uses L<Catalyst::Action::Serialize>. If you want to override
700either behavior, simply implement your own C<begin> and C<end> actions
355d4385 701and forward to another action with the Serialize and/or Deserialize
702action classes:
e601adda 703
10bcd217 704 package Foo::Controller::Monkey;
705 use Moose;
706 use namespace::autoclean;
355d4385 707
10bcd217 708 BEGIN { extends 'Catalyst::Controller::REST' }
e601adda 709
355d4385 710 sub begin : Private {
e601adda 711 my ($self, $c) = @_;
db8bb647 712 ... do things before Deserializing ...
355d4385 713 $c->forward('deserialize');
e601adda 714 ... do things after Deserializing ...
db8bb647 715 }
e601adda 716
355d4385 717 sub deserialize : ActionClass('Deserialize') {}
718
e601adda 719 sub end :Private {
720 my ($self, $c) = @_;
db8bb647 721 ... do things before Serializing ...
355d4385 722 $c->forward('serialize');
e601adda 723 ... do things after Serializing ...
724 }
725
355d4385 726 sub serialize : ActionClass('Serialize') {}
727
8bf1f20e 728If you need to deserialize multipart requests (i.e. REST data in
729one part and file uploads in others) you can do so by using the
730L<Catalyst::Action::DeserializeMultiPart> action class.
731
e540a1fa 732=back
733
e601adda 734=head1 A MILD WARNING
735
736I have code in production using L<Catalyst::Controller::REST>. That said,
737it is still under development, and it's possible that things may change
d6ece98c 738between releases. I promise to not break things unnecessarily. :)
e601adda 739
398c5a1b 740=head1 SEE ALSO
741
742L<Catalyst::Action::REST>, L<Catalyst::Action::Serialize>,
743L<Catalyst::Action::Deserialize>
744
745For help with REST in general:
746
747The HTTP 1.1 Spec is required reading. http://www.w3.org/Protocols/rfc2616/rfc2616.txt
748
749Wikipedia! http://en.wikipedia.org/wiki/Representational_State_Transfer
750
751The REST Wiki: http://rest.blueoxen.net/cgi-bin/wiki.pl?FrontPage
752
5cb5f6bb 753=head1 AUTHORS
e540a1fa 754
5cb5f6bb 755See L<Catalyst::Action::REST> for authors.
e540a1fa 756
398c5a1b 757=head1 LICENSE
758
759You may distribute this code under the same terms as Perl itself.
760
761=cut
762
24748286 763__PACKAGE__->meta->make_immutable;
764
256c894f 7651;