Merge branch 'lairdm-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.
150For instance, to relax permissions when deserializing input, add:
151 __PACKAGE__->config(
152 json_options => { relaxed => 1 }
153 )
154
d0d292d4 155=item * C<text/javascript> => C<JSONP>
156
157If a callback=? parameter is passed, this returns javascript in the form of: $callback($serializedJSON);
158
92d78e8f 159Note - this is disabled by default as it can be a security risk if you are unaware.
160
161The usual MIME types for this serialization format are: 'text/javascript', 'application/x-javascript',
162'application/javascript'.
163
95318468 164=item * C<text/x-data-dumper> => C<Data::Serializer>
e601adda 165
166Uses the L<Data::Serializer> module to generate L<Data::Dumper> output.
167
95318468 168=item * C<text/x-data-denter> => C<Data::Serializer>
e601adda 169
170Uses the L<Data::Serializer> module to generate L<Data::Denter> output.
171
95318468 172=item * C<text/x-data-taxi> => C<Data::Serializer>
e601adda 173
174Uses the L<Data::Serializer> module to generate L<Data::Taxi> output.
175
95318468 176=item * C<text/x-config-general> => C<Data::Serializer>
e601adda 177
178Uses the L<Data::Serializer> module to generate L<Config::General> output.
179
95318468 180=item * C<text/x-php-serialization> => C<Data::Serializer>
e601adda 181
182Uses the L<Data::Serializer> module to generate L<PHP::Serialization> output.
183
95318468 184=item * C<text/xml> => C<XML::Simple>
e601adda 185
186Uses L<XML::Simple> to generate XML output. This is probably not suitable
187for any real heavy XML work. Due to L<XML::Simple>s requirement that the data
188you serialize be a HASHREF, we transform outgoing data to be in the form of:
189
190 { data => $yourdata }
191
95318468 192=item * L<View>
9a76221e 193
db8bb647 194Uses a regular Catalyst view. For example, if you wanted to have your
3d8a0645 195C<text/html> and C<text/xml> views rendered by TT, set:
196
197 __PACKAGE__->config(
198 map => {
199 'text/html' => [ 'View', 'TT' ],
200 'text/xml' => [ 'View', 'XML' ],
201 }
5cb5f6bb 202 );
3d8a0645 203
204Your views should have a C<process> method like this:
205
206 sub process {
207 my ( $self, $c, $stash_key ) = @_;
5cb5f6bb 208
3d8a0645 209 my $output;
210 eval {
211 $output = $self->serialize( $c->stash->{$stash_key} );
212 };
213 return $@ if $@;
5cb5f6bb 214
3d8a0645 215 $c->response->body( $output );
216 return 1; # important
217 }
259c53c7 218
3d8a0645 219 sub serialize {
220 my ( $self, $data ) = @_;
5cb5f6bb 221
3d8a0645 222 my $serialized = ... process $data here ...
5cb5f6bb 223
3d8a0645 224 return $serialized;
225 }
9a76221e 226
178f8470 227=item * Callback
228
229For infinite flexibility, you can provide a callback for the
230deserialization/serialization steps.
231
232 __PACKAGE__->config(
233 map => {
234 'text/xml' => [ 'Callback', { deserialize => \&parse_xml, serialize => \&render_xml } ],
235 }
236 );
237
238The C<deserialize> callback is passed a string that is the body of the
239request and is expected to return a scalar value that results from
240the deserialization. The C<serialize> callback is passed the data
241structure that needs to be serialized and must return a string suitable
242for returning in the HTTP response. In addition to receiving the scalar
243to act on, both callbacks are passed the controller object and the context
244(i.e. C<$c>) as the second and third arguments.
245
e601adda 246=back
247
259c53c7 248By default, L<Catalyst::Controller::REST> will return a
95318468 249C<415 Unsupported Media Type> response if an attempt to use an unsupported
250content-type is made. You can ensure that something is always returned by
251setting the C<default> config option:
398c5a1b 252
5cb5f6bb 253 __PACKAGE__->config(default => 'text/x-yaml');
398c5a1b 254
95318468 255would make it always fall back to the serializer plugin defined for
256C<text/x-yaml>.
398c5a1b 257
e601adda 258=head1 CUSTOM SERIALIZERS
259
95318468 260Implementing new Serialization formats is easy! Contributions
259c53c7 261are most welcome! If you would like to implement a custom serializer,
95318468 262you should create two new modules in the L<Catalyst::Action::Serialize>
263and L<Catalyst::Action::Deserialize> namespace. Then assign your new
264class to the content-type's you want, and you're done.
265
259c53c7 266See L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize>
95318468 267for more information.
e601adda 268
398c5a1b 269=head1 STATUS HELPERS
270
e601adda 271Since so much of REST is in using HTTP, we provide these Status Helpers.
272Using them will ensure that you are responding with the proper codes,
273headers, and entities.
274
398c5a1b 275These helpers try and conform to the HTTP 1.1 Specification. You can
db8bb647 276refer to it at: L<http://www.w3.org/Protocols/rfc2616/rfc2616.txt>.
398c5a1b 277These routines are all implemented as regular subroutines, and as
278such require you pass the current context ($c) as the first argument.
279
5cb5f6bb 280=over
398c5a1b 281
282=cut
283
930013e6 284BEGIN { extends 'Catalyst::Controller' }
d4611771 285use Params::Validate qw(SCALAR OBJECT);
256c894f 286
287__PACKAGE__->mk_accessors(qw(serialize));
288
289__PACKAGE__->config(
e540a1fa 290 'stash_key' => 'rest',
291 'map' => {
e540a1fa 292 'text/xml' => 'XML::Simple',
e540a1fa 293 'application/json' => 'JSON',
294 'text/x-json' => 'JSON',
e540a1fa 295 },
c0008fc7 296 'compliance_mode' => 0,
256c894f 297);
298
e540a1fa 299sub begin : ActionClass('Deserialize') { }
5511d1ff 300
0ba73721 301sub end : ActionClass('Serialize') { }
302
398c5a1b 303=item status_ok
304
305Returns a "200 OK" response. Takes an "entity" to serialize.
306
307Example:
308
309 $self->status_ok(
db8bb647 310 $c,
398c5a1b 311 entity => {
312 radiohead => "Is a good band!",
313 }
314 );
315
316=cut
317
318sub status_ok {
319 my $self = shift;
e601adda 320 my $c = shift;
d4611771 321 my %p = Params::Validate::validate( @_, { entity => 1, }, );
398c5a1b 322
323 $c->response->status(200);
e601adda 324 $self->_set_entity( $c, $p{'entity'} );
398c5a1b 325 return 1;
326}
327
328=item status_created
329
330Returns a "201 CREATED" response. Takes an "entity" to serialize,
331and a "location" where the created object can be found.
332
333Example:
334
335 $self->status_created(
db8bb647 336 $c,
259c53c7 337 location => $c->req->uri,
398c5a1b 338 entity => {
339 radiohead => "Is a good band!",
340 }
341 );
342
343In the above example, we use the requested URI as our location.
344This is probably what you want for most PUT requests.
345
346=cut
bb4130f6 347
5511d1ff 348sub status_created {
349 my $self = shift;
e601adda 350 my $c = shift;
d4611771 351 my %p = Params::Validate::validate(
e601adda 352 @_,
5511d1ff 353 {
e601adda 354 location => { type => SCALAR | OBJECT },
355 entity => { optional => 1 },
5511d1ff 356 },
357 );
256c894f 358
5511d1ff 359 $c->response->status(201);
259c53c7 360 $c->response->header( 'Location' => $p{location} );
e601adda 361 $self->_set_entity( $c, $p{'entity'} );
bb4130f6 362 return 1;
363}
364
398c5a1b 365=item status_accepted
366
367Returns a "202 ACCEPTED" response. Takes an "entity" to serialize.
259c53c7 368Also takes optional "location" for queue type scenarios.
398c5a1b 369
370Example:
371
372 $self->status_accepted(
db8bb647 373 $c,
259c53c7 374 location => $c->req->uri,
398c5a1b 375 entity => {
376 status => "queued",
377 }
378 );
379
380=cut
e601adda 381
398c5a1b 382sub status_accepted {
bb4130f6 383 my $self = shift;
e601adda 384 my $c = shift;
259c53c7 385 my %p = Params::Validate::validate(
386 @_,
387 {
388 location => { type => SCALAR | OBJECT, optional => 1 },
389 entity => 1,
390 },
391 );
bb4130f6 392
398c5a1b 393 $c->response->status(202);
259c53c7 394 $c->response->header( 'Location' => $p{location} ) if exists $p{location};
e601adda 395 $self->_set_entity( $c, $p{'entity'} );
bb4130f6 396 return 1;
397}
398
bbf0feae 399=item status_no_content
400
401Returns a "204 NO CONTENT" response.
402
403=cut
404
405sub status_no_content {
406 my $self = shift;
407 my $c = shift;
408 $c->response->status(204);
409 $self->_set_entity( $c, undef );
042656b6 410 return 1;
bbf0feae 411}
412
bdff70a9 413=item status_multiple_choices
414
415Returns a "300 MULTIPLE CHOICES" response. Takes an "entity" to serialize, which should
416provide list of possible locations. Also takes optional "location" for preferred choice.
417
418=cut
419
420sub status_multiple_choices {
421 my $self = shift;
422 my $c = shift;
423 my %p = Params::Validate::validate(
424 @_,
425 {
426 entity => 1,
427 location => { type => SCALAR | OBJECT, optional => 1 },
428 },
429 );
430
bdff70a9 431 $c->response->status(300);
259c53c7 432 $c->response->header( 'Location' => $p{location} ) if exists $p{'location'};
bdff70a9 433 $self->_set_entity( $c, $p{'entity'} );
434 return 1;
435}
436
e52456a4 437=item status_found
438
439Returns a "302 FOUND" response. Takes an "entity" to serialize.
259c53c7 440Also takes optional "location".
e52456a4 441
442=cut
443
444sub status_found {
445 my $self = shift;
446 my $c = shift;
447 my %p = Params::Validate::validate(
448 @_,
449 {
450 entity => 1,
451 location => { type => SCALAR | OBJECT, optional => 1 },
452 },
453 );
454
e52456a4 455 $c->response->status(302);
259c53c7 456 $c->response->header( 'Location' => $p{location} ) if exists $p{'location'};
e52456a4 457 $self->_set_entity( $c, $p{'entity'} );
458 return 1;
459}
460
398c5a1b 461=item status_bad_request
462
463Returns a "400 BAD REQUEST" response. Takes a "message" argument
464as a scalar, which will become the value of "error" in the serialized
465response.
466
467Example:
468
469 $self->status_bad_request(
db8bb647 470 $c,
33e5de96 471 message => "Cannot do what you have asked!",
398c5a1b 472 );
473
474=cut
e601adda 475
cc186a5b 476sub status_bad_request {
477 my $self = shift;
e601adda 478 my $c = shift;
d4611771 479 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
cc186a5b 480
481 $c->response->status(400);
faf5c20b 482 $c->log->debug( "Status Bad Request: " . $p{'message'} ) if $c->debug;
e601adda 483 $self->_set_entity( $c, { error => $p{'message'} } );
cc186a5b 484 return 1;
485}
486
550807bc 487=item status_forbidden
488
489Returns a "403 FORBIDDEN" response. Takes a "message" argument
490as a scalar, which will become the value of "error" in the serialized
491response.
492
493Example:
494
495 $self->status_forbidden(
496 $c,
497 message => "access denied",
498 );
499
500=cut
501
502sub status_forbidden {
503 my $self = shift;
504 my $c = shift;
505 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
506
507 $c->response->status(403);
508 $c->log->debug( "Status Forbidden: " . $p{'message'} ) if $c->debug;
509 $self->_set_entity( $c, { error => $p{'message'} } );
510 return 1;
511}
512
398c5a1b 513=item status_not_found
514
515Returns a "404 NOT FOUND" response. Takes a "message" argument
516as a scalar, which will become the value of "error" in the serialized
517response.
518
519Example:
520
521 $self->status_not_found(
db8bb647 522 $c,
33e5de96 523 message => "Cannot find what you were looking for!",
398c5a1b 524 );
525
526=cut
e601adda 527
bb4130f6 528sub status_not_found {
529 my $self = shift;
e601adda 530 my $c = shift;
d4611771 531 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
bb4130f6 532
533 $c->response->status(404);
faf5c20b 534 $c->log->debug( "Status Not Found: " . $p{'message'} ) if $c->debug;
e601adda 535 $self->_set_entity( $c, { error => $p{'message'} } );
bb4130f6 536 return 1;
537}
538
bbf0feae 539=item gone
540
541Returns a "41O GONE" response. Takes a "message" argument as a scalar,
542which will become the value of "error" in the serialized response.
543
544Example:
545
546 $self->status_gone(
547 $c,
548 message => "The document have been deleted by foo",
549 );
550
551=cut
552
553sub status_gone {
554 my $self = shift;
555 my $c = shift;
556 my %p = Params::Validate::validate( @_, { message => { type => SCALAR }, }, );
557
558 $c->response->status(410);
559 $c->log->debug( "Status Gone " . $p{'message'} ) if $c->debug;
560 $self->_set_entity( $c, { error => $p{'message'} } );
561 return 1;
562}
563
0aceaa9b 564=item status_see_other
565
566Returns a "303 See Other" response. Takes an optional "entity" to serialize,
567and a "location" where the client should redirect to.
568
569Example:
570
571 $self->status_see_other(
572 $c,
573 location => $some_other_url,
574 entity => {
575 radiohead => "Is a good band!",
576 }
577 );
578
579=cut
580
581sub status_see_other {
582 my $self = shift;
583 my $c = shift;
584 my %p = Params::Validate::validate(
585 @_,
586 {
587 location => { type => SCALAR | OBJECT },
588 entity => { optional => 1 },
589 },
590 );
591
592 $c->response->status(303);
593 $c->response->header( 'Location' => $p{location} );
594 $self->_set_entity( $c, $p{'entity'} );
595 return 1;
596}
597
598=item status_moved
599
600Returns a "301 MOVED" response. Takes an "entity" to serialize, and a
601"location" where the created object can be found.
602
603Example:
604
605 $self->status_moved(
606 $c,
607 location => '/somewhere/else',
608 entity => {
609 radiohead => "Is a good band!",
610 },
611 );
612
613=cut
614
615sub status_moved {
616 my $self = shift;
617 my $c = shift;
618 my %p = Params::Validate::validate(
619 @_,
620 {
621 location => { type => SCALAR | OBJECT },
622 entity => { optional => 1 },
623 },
624 );
625
626 my $location = ref $p{location}
627 ? $p{location}->as_string
628 : $p{location}
629 ;
630
631 $c->response->status(301);
632 $c->response->header( Location => $location );
633 $self->_set_entity($c, $p{entity});
634 return 1;
635}
636
bb4130f6 637sub _set_entity {
e601adda 638 my $self = shift;
639 my $c = shift;
bb4130f6 640 my $entity = shift;
e601adda 641 if ( defined($entity) ) {
faf5c20b 642 $c->stash->{ $self->{'stash_key'} } = $entity;
5511d1ff 643 }
644 return 1;
eccb2137 645}
256c894f 646
398c5a1b 647=back
648
649=head1 MANUAL RESPONSES
650
651If you want to construct your responses yourself, all you need to
652do is put the object you want serialized in $c->stash->{'rest'}.
653
e601adda 654=head1 IMPLEMENTATION DETAILS
655
656This Controller ties together L<Catalyst::Action::REST>,
657L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize>. It should be suitable for most applications. You should be aware that it:
658
659=over 4
660
661=item Configures the Serialization Actions
662
663This class provides a default configuration for Serialization. It is currently:
664
665 __PACKAGE__->config(
95318468 666 'stash_key' => 'rest',
667 'map' => {
668 'text/html' => 'YAML::HTML',
669 'text/xml' => 'XML::Simple',
670 'text/x-yaml' => 'YAML',
671 'application/json' => 'JSON',
672 'text/x-json' => 'JSON',
673 'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ],
674 'text/x-data-denter' => [ 'Data::Serializer', 'Data::Denter' ],
675 'text/x-data-taxi' => [ 'Data::Serializer', 'Data::Taxi' ],
676 'application/x-storable' => [ 'Data::Serializer', 'Storable' ],
677 'application/x-freezethaw' => [ 'Data::Serializer', 'FreezeThaw' ],
678 'text/x-config-general' => [ 'Data::Serializer', 'Config::General' ],
679 'text/x-php-serialization' => [ 'Data::Serializer', 'PHP::Serialization' ],
680 },
e601adda 681 );
682
683You can read the full set of options for this configuration block in
684L<Catalyst::Action::Serialize>.
685
686=item Sets a C<begin> and C<end> method for you
687
688The C<begin> method uses L<Catalyst::Action::Deserialize>. The C<end>
689method uses L<Catalyst::Action::Serialize>. If you want to override
690either behavior, simply implement your own C<begin> and C<end> actions
355d4385 691and forward to another action with the Serialize and/or Deserialize
692action classes:
e601adda 693
10bcd217 694 package Foo::Controller::Monkey;
695 use Moose;
696 use namespace::autoclean;
355d4385 697
10bcd217 698 BEGIN { extends 'Catalyst::Controller::REST' }
e601adda 699
355d4385 700 sub begin : Private {
e601adda 701 my ($self, $c) = @_;
db8bb647 702 ... do things before Deserializing ...
355d4385 703 $c->forward('deserialize');
e601adda 704 ... do things after Deserializing ...
db8bb647 705 }
e601adda 706
355d4385 707 sub deserialize : ActionClass('Deserialize') {}
708
e601adda 709 sub end :Private {
710 my ($self, $c) = @_;
db8bb647 711 ... do things before Serializing ...
355d4385 712 $c->forward('serialize');
e601adda 713 ... do things after Serializing ...
714 }
715
355d4385 716 sub serialize : ActionClass('Serialize') {}
717
8bf1f20e 718If you need to deserialize multipart requests (i.e. REST data in
719one part and file uploads in others) you can do so by using the
720L<Catalyst::Action::DeserializeMultiPart> action class.
721
e540a1fa 722=back
723
e601adda 724=head1 A MILD WARNING
725
726I have code in production using L<Catalyst::Controller::REST>. That said,
727it is still under development, and it's possible that things may change
d6ece98c 728between releases. I promise to not break things unnecessarily. :)
e601adda 729
398c5a1b 730=head1 SEE ALSO
731
732L<Catalyst::Action::REST>, L<Catalyst::Action::Serialize>,
733L<Catalyst::Action::Deserialize>
734
735For help with REST in general:
736
737The HTTP 1.1 Spec is required reading. http://www.w3.org/Protocols/rfc2616/rfc2616.txt
738
739Wikipedia! http://en.wikipedia.org/wiki/Representational_State_Transfer
740
741The REST Wiki: http://rest.blueoxen.net/cgi-bin/wiki.pl?FrontPage
742
5cb5f6bb 743=head1 AUTHORS
e540a1fa 744
5cb5f6bb 745See L<Catalyst::Action::REST> for authors.
e540a1fa 746
398c5a1b 747=head1 LICENSE
748
749You may distribute this code under the same terms as Perl itself.
750
751=cut
752
24748286 753__PACKAGE__->meta->make_immutable;
754
256c894f 7551;