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