@@ -1,3 +1,40 @@
+Mon 8 Feb 2010 22:17:12 GMT - Release 0.83
+ Make it possible to deserialize a request with a DELETE method. This probably
+ breaks 'strict' REST guidelines, but is useful for being able to delete multiple
+ resources from a single call by providing a batch delete method.
+
+ Remove JSONP from the list of default serializers (RT#54336)
+
+ Fix MANIFEST (RT#54408)
+
+Thu 4 Feb 2010 22:31:57 GMT - Release 0.82
+
+ Integrated Catalyst::Request::REST::ForBrowsers as
+ Catalyst::TraitFor::Request::ForBrowsers. (Dave Rolsky)
+
+ Clarified docs so that they encourage the use of the request traits, rather
+ than using Catalyst::Request::REST. (Dave Rolsky)
+
+ When Catalyst::Action::REST or Controller::REST automatically add the trait,
+ your request class will no longer end up getting set to
+ Catalyst::Request::REST. Instead, creates an anon class with the appropriate
+ role. (Dave Rolsky)
+
+ Shut up log output from the tests. (Dave Rolsky)
+
+ Added a $VERSION to every module, mostly to make sure that when people
+ install Catalyst::Request::REST::ForBrowsers, they get the version in this
+ distro. (Dave Rolsky)
+
+ Change Catalyst::Action::Serialize, Catalyst::Action::Deserialize and
+ Catalyst::Action::SerializeBase to be more Moose like.
+
+ Fix JSON and JSON::XS to encode_blessed. (fREW)
+
+ Fix Catalyst::Action::Serialize to use objects instead of classes. (fREW)
+
+ Fix doc nits. (RT#53780)
+
Thu 14 Jan 20:56:00 GMT 2010 - Release 0.81
Add a JSONP serialization type.
@@ -120,93 +157,93 @@ Thu Jan 3 17:23:58 PST 2008 (adam) - Release 0.60
Prepped for release.
Thu Jan 3 19:42:16 EST 2008
- Fixed RT#30498 - REST controller references Catalyst without
- loading it first.
- Fixed RT#32042 - Import of Params::Validate :all plays badly
- with subclasses that have their own validate()
- Fixed RT#30456 - Debug messages print even with debugging disabled
+ Fixed RT#30498 - REST controller references Catalyst without
+ loading it first.
+ Fixed RT#32042 - Import of Params::Validate :all plays badly
+ with subclasses that have their own validate()
+ Fixed RT#30456 - Debug messages print even with debugging disabled
Thu Jan 3 08:54:09 PST 2008
- Fixed an issue where YAML::Syck versions 0.92 require $c->request->body to
- be stringified
+ Fixed an issue where YAML::Syck versions 0.92 require $c->request->body to
+ be stringified
Fri Dec 21 15:23:46 EDT 2007
- Updated the configuration specifiers to operate more in line with the way
- Catalyst expects. Most notably component based configuration through
- "Controller::RestClass" now works. "serialize" at the top level simply
- is suggested defaults that all REST classes inherit.
+ Updated the configuration specifiers to operate more in line with the way
+ Catalyst expects. Most notably component based configuration through
+ "Controller::RestClass" now works. "serialize" at the top level simply
+ is suggested defaults that all REST classes inherit.
Wed Jul 04 11:17:20 EDT 2007
- Fixed 'default' serializer to set a valid Content-Type: header. Fixes
- RT ticket 27949. Note that behavior has changed -- the default
- serializer must now be specified as a content-type, not as a plugin
- name. (dmo@roaringpenguin.com)
+ Fixed 'default' serializer to set a valid Content-Type: header. Fixes
+ RT ticket 27949. Note that behavior has changed -- the default
+ serializer must now be specified as a content-type, not as a plugin
+ name. (dmo@roaringpenguin.com)
Thu May 24 14:01:06 PDT 2007 (adam) - Release 0.41
- Moved a bogus $self->class to $c->component($self->class)
+ Moved a bogus $self->class to $c->component($self->class)
Fri Mar 9 14:13:29 PST 2007 (adam) - Release 0.40
- Refactored the Content-Type negotiation to live in Catalyst::Request::REST. (drolsky)
- Added some useful debugging. (drolsky)
- Added a View serializer/deserializer, which simply calls the correct
- Catalyst view. ('text/html' => [ 'View', 'TT' ]) (claco, adam)
+ Refactored the Content-Type negotiation to live in Catalyst::Request::REST. (drolsky)
+ Added some useful debugging. (drolsky)
+ Added a View serializer/deserializer, which simply calls the correct
+ Catalyst view. ('text/html' => [ 'View', 'TT' ]) (claco, adam)
Wed Dec 6 00:45:02 PST 2006 (adam) - Release 0.31
- Fixed a bug where we would report a blank content-type negotiation.
- Added Data::Dump as a dependency.
+ Fixed a bug where we would report a blank content-type negotiation.
+ Added Data::Dump as a dependency.
Tue Dec 5 13:02:08 PST 2006 (adam)
- Made the YAML::HTML view automatically append content-type=text/html on
- the resulting URLs.
+ Made the YAML::HTML view automatically append content-type=text/html on
+ the resulting URLs.
Sun Dec 3 12:24:16 PST 2006 (adam) - Release 0.30
- Updated the Makefile to support optional installation of the different
- Serialization formats.
- Renamed some of the test cases, since the execution order doesn't
- matter.
- Fixed things so that not having a Serialization module returns 415.
- Fixed things so that failure to Deserialize sends the proper status.
- Refactored the Plugin loading to Catalyst::Action::SerializeBase.
- Updated the Documentation.
- Added a whole raft of serializers. (JSON, all the Data::Serializer
- supported ones, and XML::Simple)
- Added test cases.
+ Updated the Makefile to support optional installation of the different
+ Serialization formats.
+ Renamed some of the test cases, since the execution order doesn't
+ matter.
+ Fixed things so that not having a Serialization module returns 415.
+ Fixed things so that failure to Deserialize sends the proper status.
+ Refactored the Plugin loading to Catalyst::Action::SerializeBase.
+ Updated the Documentation.
+ Added a whole raft of serializers. (JSON, all the Data::Serializer
+ supported ones, and XML::Simple)
+ Added test cases.
Thu Nov 30 23:51:04 PST 2006 (adam)
- Refactored the Catalyst::Action::REST dispatch, so that the default
- method is called before any _METHOD handlers. In addition, moved
- the 405 Not Implemented handler to be foo_not_implemented, instead
- of the default sub. (daisuke++ pointed out the inconsistency and
- provided a patch, and I added the foo_not_implemented support)
+ Refactored the Catalyst::Action::REST dispatch, so that the default
+ method is called before any _METHOD handlers. In addition, moved
+ the 405 Not Implemented handler to be foo_not_implemented, instead
+ of the default sub. (daisuke++ pointed out the inconsistency and
+ provided a patch, and I added the foo_not_implemented support)
- Added in automated OPTIONS handler, which constructs the allow
- header for you, just like the 405 handler. Can be overridden
- with a normal _METHOD sub.
+ Added in automated OPTIONS handler, which constructs the allow
+ header for you, just like the 405 handler. Can be overridden
+ with a normal _METHOD sub.
- Refactored Test::Rest, so that it uses closures to create the
- very similar $test->method() subs.
+ Refactored Test::Rest, so that it uses closures to create the
+ very similar $test->method() subs.
- Added tests for Catalyst::Action::REST.
+ Added tests for Catalyst::Action::REST.
Thu Nov 30 17:14:51 PST 2006 (adam) - Release 0.2
- Added documentation patch from Daisuke Maki (daisuke@endeworks.jp)
- Added dependency patch from Daisuke Maki (daisuke@endeworks.jp)
+ Added documentation patch from Daisuke Maki (daisuke@endeworks.jp)
+ Added dependency patch from Daisuke Maki (daisuke@endeworks.jp)
Sun Nov 19 16:24:20 PST 2006 (adam) - Release 0.1
- Added status_accepted (Code 202)
- Added a first pass at documentation.
+ Added status_accepted (Code 202)
+ Added a first pass at documentation.
Mon Oct 16 14:48:54 PDT 2006 (adam)
- Added in Test Suite
- Created Catalyst::Action::Serialize and Catalyst::Action::Deserialize
- Added Data::Serializer actions
- Added status_created helper method
+ Added in Test Suite
+ Created Catalyst::Action::Serialize and Catalyst::Action::Deserialize
+ Added Data::Serializer actions
+ Added status_created helper method
Wed Oct 18 17:29:07 PDT 2006 (adam)
- Added more status_ helpers
+ Added more status_ helpers
Thu Oct 19 16:04:33 PDT 2006 (adam)
- Converted error helpers to return an object instead of plain-text. It's
- a more consistent model than a text/plain error message.
- Added logging to 4xx status handlers
+ Converted error helpers to return an object instead of plain-text. It's
+ a more consistent model than a text/plain error message.
+ Added logging to 4xx status handlers
@@ -2,6 +2,7 @@ Changes
inc/Module/AutoInstall.pm
inc/Module/Install.pm
inc/Module/Install/AuthorRequires.pm
+inc/Module/Install/AuthorTests.pm
inc/Module/Install/AutoInstall.pm
inc/Module/Install/Base.pm
inc/Module/Install/Can.pm
@@ -31,7 +32,9 @@ lib/Catalyst/Action/Serialize/YAML/HTML.pm
lib/Catalyst/Action/SerializeBase.pm
lib/Catalyst/Controller/REST.pm
lib/Catalyst/Request/REST.pm
+lib/Catalyst/Request/REST/ForBrowsers.pm
lib/Catalyst/TraitFor/Request/REST.pm
+lib/Catalyst/TraitFor/Request/REST/ForBrowsers.pm
Makefile.PL
MANIFEST This list of files
META.yml
@@ -45,7 +48,8 @@ t/catalyst-action-serialize-accept.t
t/catalyst-action-serialize-query.t
t/catalyst-action-serialize.t
t/catalyst-controller-rest.t
-t/catalyst-request-rest.t
+t/catalyst-traitfor-request-rest-forbrowsers.t
+t/catalyst-traitfor-request-rest.t
t/data-serializer.t
t/isa.t
t/json.t
@@ -54,17 +58,21 @@ t/lib/Test/Action/Class.pm
t/lib/Test/Catalyst/Action/REST.pm
t/lib/Test/Catalyst/Action/REST/Controller/Actions.pm
t/lib/Test/Catalyst/Action/REST/Controller/Deserialize.pm
+t/lib/Test/Catalyst/Action/REST/Controller/Override.pm
t/lib/Test/Catalyst/Action/REST/Controller/REST.pm
t/lib/Test/Catalyst/Action/REST/Controller/Root.pm
t/lib/Test/Catalyst/Action/REST/Controller/Serialize.pm
+t/lib/Test/Catalyst/Log.pm
t/lib/Test/Rest.pm
t/lib/Test/Serialize.pm
t/lib/Test/Serialize/Controller/REST.pm
t/lib/Test/Serialize/View/Awful.pm
t/lib/Test/Serialize/View/Simple.pm
-t/pod.t
t/view.t
t/xml-simple.t
t/yaml-html.t
t/yaml.t
TODO
+xt/pod-spell.t
+xt/pod.t
+xt/version-numbers.t
@@ -18,6 +18,7 @@ no_index:
directory:
- inc
- t
+ - xt
requires:
Catalyst::Runtime: 5.80
Class::Inspector: 1.13
@@ -34,4 +35,4 @@ requires:
resources:
license: http://dev.perl.org/licenses/
repository: git://git.shadowcat.co.uk/catagits/Catalyst-Action-REST.git
-version: 0.81
+version: 0.83
@@ -2,6 +2,7 @@ use strict;
use warnings;
use inc::Module::Install;
use Module::Install::AuthorRequires;
+use Module::Install::AuthorTests;
perl_version '5.8.1';
@@ -23,6 +24,12 @@ requires('MRO::Compat' => '0.10');
requires 'namespace::autoclean';
test_requires 'Test::More' => '0.88';
+author_requires 'Test::Pod' => 1.14;
+author_requires 'Module::Info';
+author_requires 'File::Find::Rule';
+
+author_tests 'xt/';
+
feature 'JSON (application/json) support',
-default => 0,
'JSON' => '2.12';
@@ -42,8 +42,8 @@ DESCRIPTION
which brings this class together with automatic Serialization of
requests and responses.
- When you use this module, the request class will be changed to
- Catalyst::Request::REST.
+ When you use this module, it adds the Catalyst::TraitFor::Request::REST
+ role to your request class.
METHODS
dispatch
@@ -54,6 +54,11 @@ SEE ALSO
You likely want to look at Catalyst::Controller::REST, which implements
a sensible set of defaults for a controller doing REST.
+ This class automatically adds the Catalyst::TraitFor::Request::REST role
+ to your request class. If you're writing a webapp which provides RESTful
+ responses and still needs to accommodate web browsers, you may prefer to
+ use Catalyst::TraitFor::Request::REST::ForBrowsers instead.
+
Catalyst::Action::Serialize, Catalyst::Action::Deserialize
TROUBLESHOOTING
@@ -76,21 +81,23 @@ AUTHOR
(<http://www.marchex.com>)
CONTRIBUTORS
- Arthur Axel "fREW" Schmidt <frioux@gmail.com>
+ Tomas Doran (t0m) <bobtfish@bobtfish.net>
+
+ John Goulah
Christopher Laco
- Luke Saunders
+ Daisuke Maki <daisuke@endeworks.jp>
- John Goulah
+ Hans Dieter Pearcey
- Daisuke Maki <daisuke@endeworks.jp>
+ Dave Rolsky <autarch@urth.org>
- J. Shirley <jshirley@gmail.com>
+ Luke Saunders
- Hans Dieter Pearcey
+ Arthur Axel "fREW" Schmidt <frioux@gmail.com>
- Tomas Doran (t0m) <bobtfish@bobtfish.net>
+ J. Shirley <jshirley@gmail.com>
COPYRIGHT
Copyright the above named AUTHOR and CONTRIBUTORS
@@ -0,0 +1,59 @@
+#line 1
+package Module::Install::AuthorTests;
+
+use 5.005;
+use strict;
+use Module::Install::Base;
+use Carp ();
+
+#line 16
+
+use vars qw{$VERSION $ISCORE @ISA};
+BEGIN {
+ $VERSION = '0.002';
+ $ISCORE = 1;
+ @ISA = qw{Module::Install::Base};
+}
+
+#line 42
+
+sub author_tests {
+ my ($self, @dirs) = @_;
+ _add_author_tests($self, \@dirs, 0);
+}
+
+#line 56
+
+sub recursive_author_tests {
+ my ($self, @dirs) = @_;
+ _add_author_tests($self, \@dirs, 1);
+}
+
+sub _wanted {
+ my $href = shift;
+ sub { /\.t$/ and -f $_ and $href->{$File::Find::dir} = 1 }
+}
+
+sub _add_author_tests {
+ my ($self, $dirs, $recurse) = @_;
+ return unless $Module::Install::AUTHOR;
+
+ my @tests = $self->tests ? (split / /, $self->tests) : 't/*.t';
+
+ # XXX: pick a default, later -- rjbs, 2008-02-24
+ my @dirs = @$dirs ? @$dirs : Carp::confess "no dirs given to author_tests";
+ @dirs = grep { -d } @dirs;
+
+ if ($recurse) {
+ require File::Find;
+ my %test_dir;
+ File::Find::find(_wanted(\%test_dir), @dirs);
+ $self->tests( join ' ', @tests, map { "$_/*.t" } sort keys %test_dir );
+ } else {
+ $self->tests( join ' ', @tests, map { "$_/*.t" } sort @dirs );
+ }
+}
+
+#line 107
+
+1;
@@ -6,6 +6,9 @@ use namespace::autoclean;
extends 'Catalyst::Action';
use Data::Serializer;
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
sub execute {
my $self = shift;
my ( $controller, $c, $serializer ) = @_;
@@ -6,6 +6,9 @@ use namespace::autoclean;
extends 'Catalyst::Action';
use JSON qw( decode_json );
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
sub execute {
my $self = shift;
my ( $controller, $c, $test ) = @_;
@@ -5,6 +5,9 @@ use namespace::autoclean;
extends 'Catalyst::Action';
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
sub execute {
return 1;
}
@@ -5,6 +5,9 @@ use namespace::autoclean;
extends 'Catalyst::Action';
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
sub execute {
my $self = shift;
my ( $controller, $c, $test ) = @_;
@@ -6,6 +6,9 @@ use namespace::autoclean;
extends 'Catalyst::Action';
use YAML::Syck;
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
sub execute {
my $self = shift;
my ( $controller, $c, $test ) = @_;
@@ -7,13 +7,16 @@ extends 'Catalyst::Action::SerializeBase';
use Module::Pluggable::Object;
use MRO::Compat;
-__PACKAGE__->mk_accessors(qw(plugins));
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
+has plugins => ( is => 'rw' );
sub execute {
my $self = shift;
my ( $controller, $c ) = @_;
- my @demethods = qw(POST PUT OPTIONS);
+ my @demethods = qw(POST PUT OPTIONS DELETE);
my $method = $c->request->method;
if ( grep /^$method$/, @demethods ) {
my ( $sclass, $sarg, $content_type ) =
@@ -38,6 +41,8 @@ sub execute {
return 1;
}
+__PACKAGE__->meta->make_immutable;
+
=head1 NAME
Catalyst::Action::Deserialize - Deserialize Data in a Request
@@ -98,5 +103,3 @@ See L<Catalyst::Action::REST> for authors.
You may distribute this code under the same terms as Perl itself.
=cut
-
-1;
@@ -10,7 +10,7 @@ use Catalyst::Controller::REST;
BEGIN { require 5.008001; }
-our $VERSION = '0.81';
+our $VERSION = '0.83';
$VERSION = eval $VERSION;
sub new {
@@ -67,8 +67,8 @@ It is likely that you really want to look at L<Catalyst::Controller::REST>,
which brings this class together with automatic Serialization of requests
and responses.
-When you use this module, the request class will be changed to
-L<Catalyst::Request::REST>.
+When you use this module, it adds the L<Catalyst::TraitFor::Request::REST>
+role to your request class.
=head1 METHODS
@@ -156,8 +156,13 @@ sub _return_not_implemented {
=head1 SEE ALSO
-You likely want to look at L<Catalyst::Controller::REST>, which implements
-a sensible set of defaults for a controller doing REST.
+You likely want to look at L<Catalyst::Controller::REST>, which implements a
+sensible set of defaults for a controller doing REST.
+
+This class automatically adds the L<Catalyst::TraitFor::Request::REST> role to
+your request class. If you're writing a webapp which provides RESTful
+responses and still needs to accommodate web browsers, you may prefer to use
+L<Catalyst::TraitFor::Request::REST::ForBrowsers> instead.
L<Catalyst::Action::Serialize>, L<Catalyst::Action::Deserialize>
@@ -180,27 +185,29 @@ for this to run smoothly.
=head1 AUTHOR
-Adam Jacob <adam@stalecoffee.org>, with lots of help from mst and jrockway
+Adam Jacob E<lt>adam@stalecoffee.orgE<gt>, with lots of help from mst and jrockway
Marchex, Inc. paid me while I developed this module. (L<http://www.marchex.com>)
=head1 CONTRIBUTORS
-Arthur Axel "fREW" Schmidt <frioux@gmail.com>
+Tomas Doran (t0m) E<lt>bobtfish@bobtfish.netE<gt>
+
+John Goulah
Christopher Laco
-Luke Saunders
+Daisuke Maki E<lt>daisuke@endeworks.jpE<gt>
-John Goulah
+Hans Dieter Pearcey
-Daisuke Maki <daisuke@endeworks.jp>
+Dave Rolsky E<lt>autarch@urth.orgE<gt>
-J. Shirley <jshirley@gmail.com>
+Luke Saunders
-Hans Dieter Pearcey
+Arthur Axel "fREW" Schmidt E<lt>frioux@gmail.comE<gt>
-Tomas Doran (t0m) <bobtfish@bobtfish.net>
+J. Shirley E<lt>jshirley@gmail.comE<gt>
=head1 COPYRIGHT
@@ -6,6 +6,9 @@ use namespace::autoclean;
extends 'Catalyst::Action';
use Data::Serializer;
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
sub execute {
my $self = shift;
my ( $controller, $c, $serializer ) = @_;
@@ -4,11 +4,14 @@ use Moose;
use namespace::autoclean;
extends 'Catalyst::Action::Serialize::JSON';
-use JSON::XS qw(encode_json);
+use JSON::XS ();
-sub serialize {
- my $self = shift;
- encode_json( shift );
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
+sub _build_encoder {
+ my $self = shift;
+ return JSON::XS->new->convert_blessed;
}
1;
@@ -4,7 +4,20 @@ use Moose;
use namespace::autoclean;
extends 'Catalyst::Action';
-use JSON qw(encode_json);
+use JSON ();
+
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
+has encoder => (
+ is => 'ro',
+ lazy_build => 1,
+);
+
+sub _build_encoder {
+ my $self = shift;
+ return JSON->new->utf8->convert_blessed;
+}
sub execute {
my $self = shift;
@@ -13,7 +26,7 @@ sub execute {
my $stash_key = (
$controller->{'serialize'} ?
$controller->{'serialize'}->{'stash_key'} :
- $controller->{'stash_key'}
+ $controller->{'stash_key'}
) || 'rest';
my $output = $self->serialize( $c->stash->{$stash_key} );
$c->response->output( $output );
@@ -22,7 +35,8 @@ sub execute {
sub serialize {
my $self = shift;
- encode_json( shift );
+ my $data = shift;
+ $self->encoder->encode( $data );
}
1;
@@ -4,6 +4,9 @@ use namespace::autoclean;
extends 'Catalyst::Action::Serialize::JSON';
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
after 'execute' => sub {
my $self = shift;
my ($controller, $c) = @_;
@@ -4,6 +4,9 @@ use namespace::autoclean;
extends 'Catalyst::Action';
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
sub execute {
my $self = shift;
my ( $controller, $c, $view ) = @_;
@@ -5,6 +5,9 @@ use namespace::autoclean;
extends 'Catalyst::Action';
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
sub execute {
my $self = shift;
my ( $controller, $c ) = @_;
@@ -7,6 +7,9 @@ extends 'Catalyst::Action';
use YAML::Syck;
use URI::Find;
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
sub execute {
my $self = shift;
my ( $controller, $c ) = @_;
@@ -6,6 +6,9 @@ use namespace::autoclean;
extends 'Catalyst::Action';
use YAML::Syck;
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
sub execute {
my $self = shift;
my ( $controller, $c ) = @_;
@@ -7,6 +7,15 @@ extends 'Catalyst::Action::SerializeBase';
use Module::Pluggable::Object;
use MRO::Compat;
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
+has _encoders => (
+ is => 'ro',
+ isa => 'HashRef',
+ default => sub { {} },
+);
+
sub execute {
my $self = shift;
my ( $controller, $c ) = @_;
@@ -33,12 +42,15 @@ sub execute {
$c->log->debug(
"Serializing with $sclass" . ( $sarg ? " [$sarg]" : '' ) ) if $c->debug;
+ $self->_encoders->{$sclass} ||= $sclass->new;
+ my $sobj = $self->_encoders->{$sclass};
+
my $rc;
eval {
if ( defined($sarg) ) {
- $rc = $sclass->execute( $controller, $c, $sarg );
+ $rc = $sobj->execute( $controller, $c, $sarg );
} else {
- $rc = $sclass->execute( $controller, $c );
+ $rc = $sobj->execute( $controller, $c );
}
};
if ($@) {
@@ -50,7 +62,7 @@ sub execute {
return 1;
}
-1;
+__PACKAGE__->meta->make_immutable;
=head1 NAME
@@ -104,7 +116,7 @@ Takes a hashref, mapping Content-Types to a given serializer plugin.
This is the 'fall-back' Content-Type if none of the requested or acceptable
types is found in the L</map>. It must be an entry in the L</map>.
-=head2 stash_key
+=head2 stash_key
Specifies the key of the stash entry holding the data that is to be serialized.
So if the value is "rest", we will serialize the data under:
@@ -124,7 +136,7 @@ perhaps for debugging.
Daisuke Maki pointed out that early versions of this Action did not play
well with others, or generally behave in a way that was very consistent
-with the rest of Catalyst.
+with the rest of Catalyst.
=head1 SEE ALSO
@@ -8,14 +8,16 @@ use Module::Pluggable::Object;
use Catalyst::Request::REST;
use Catalyst::Utils ();
-sub new {
- my $class = shift;
- my $config = shift;
- Catalyst::Request::REST->_insert_self_into( $config->{class} );
- return $class->SUPER::new($config, @_);
-}
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
+after BUILDARGS => sub {
+ my $class = shift;
+ my $config = shift;
+ Catalyst::Request::REST->_insert_self_into( $config->{class} );
+};
-__PACKAGE__->mk_accessors(qw(_serialize_plugins _loaded_plugins));
+has [qw(_serialize_plugins _loaded_plugins)] => ( is => 'rw' );
sub _load_content_plugins {
my $self = shift;
@@ -153,13 +155,11 @@ sub _serialize_bad_request {
return undef;
}
-1;
+__PACKAGE__->meta->make_immutable;
=head1 NAME
-B<Catalyst::Action::SerializeBase>
-
-Base class for Catalyst::Action::Serialize and Catlayst::Action::Deserialize.
+Catalyst::Action::SerializeBase - Base class for Catalyst::Action::Serialize and Catlayst::Action::Deserialize.
=head1 DESCRIPTION
@@ -2,7 +2,7 @@ package Catalyst::Controller::REST;
use Moose;
use namespace::autoclean;
-our $VERSION = '0.81';
+our $VERSION = '0.83';
$VERSION = eval $VERSION;
=head1 NAME
@@ -138,7 +138,7 @@ Returns YAML generated by L<YAML::Syck>.
=item * C<text/html> => C<YAML::HTML>
This uses L<YAML::Syck> and L<URI::Find> to generate YAML with all URLs turned
-to hyperlinks. Only useable for Serialization.
+to hyperlinks. Only usable for Serialization.
=item * C<application/json> => C<JSON>
@@ -150,6 +150,11 @@ deprecated and you will receive warnings in your log.
If a callback=? parameter is passed, this returns javascript in the form of: $callback($serializedJSON);
+Note - this is disabled by default as it can be a security risk if you are unaware.
+
+The usual MIME types for this serialization format are: 'text/javascript', 'application/x-javascript',
+'application/javascript'.
+
=item * C<text/x-data-dumper> => C<Data::Serializer>
Uses the L<Data::Serializer> module to generate L<Data::Dumper> output.
@@ -272,9 +277,6 @@ __PACKAGE__->config(
'text/x-yaml' => 'YAML',
'application/json' => 'JSON',
'text/x-json' => 'JSON',
- 'application/x-javascript' => 'JSONP',
- 'application/javascript' => 'JSONP',
- 'text/javascript' => 'JSONP',
'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ],
'text/x-data-denter' => [ 'Data::Serializer', 'Data::Denter' ],
'text/x-data-taxi' => [ 'Data::Serializer', 'Data::Taxi' ],
@@ -509,9 +511,6 @@ This class provides a default configuration for Serialization. It is currently:
'text/x-yaml' => 'YAML',
'application/json' => 'JSON',
'text/x-json' => 'JSON',
- 'application/x-javascript' => 'JSONP',
- 'application/javascript' => 'JSONP',
- 'text/javascript' => 'JSONP',
'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ],
'text/x-data-denter' => [ 'Data::Serializer', 'Data::Denter' ],
'text/x-data-taxi' => [ 'Data::Serializer', 'Data::Taxi' ],
@@ -558,7 +557,7 @@ and use MRO::Compat:
I have code in production using L<Catalyst::Controller::REST>. That said,
it is still under development, and it's possible that things may change
-between releases. I promise to not break things unneccesarily. :)
+between releases. I promise to not break things unnecessarily. :)
=head1 SEE ALSO
@@ -0,0 +1,57 @@
+package Catalyst::Request::REST::ForBrowsers;
+use Moose;
+
+use namespace::autoclean;
+
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
+extends 'Catalyst::Request::REST';
+with 'Catalyst::TraitFor::Request::REST::ForBrowsers';
+
+__PACKAGE__->meta->make_immutable;
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+Catalyst::Request::REST::ForBrowsers - A Catalyst::Request::REST subclass for dealing with browsers
+
+=head1 SYNOPSIS
+
+ package MyApp;
+
+ use Catalyst::Request::REST::ForBrowsers;
+
+ MyApp->request_class( 'Catalyst::Request::REST::ForBrowsers' );
+
+=head1 DESCRIPTION
+
+This class has been deprecated in favor of
+L<Catalyst::TraitFor::Request::REST::ForBrowsers>. Please see that class for
+details on methods and attributes.
+
+=head1 AUTHOR
+
+Dave Rolsky, C<< <autarch@urth.org> >>
+
+=head1 BUGS
+
+Please report any bugs or feature requests to
+C<bug-catalyst-request-rest-forbrowsers@rt.cpan.org>, or through the
+web interface at L<http://rt.cpan.org>. I will be notified, and then
+you'll automatically be notified of progress on your bug as I make
+changes.
+
+=head1 COPYRIGHT & LICENSE
+
+Copyright 2008-2009 Dave Rolsky, All Rights Reserved.
+
+This program is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
@@ -7,6 +7,9 @@ use namespace::autoclean;
extends 'Catalyst::Request';
with 'Catalyst::TraitFor::Request::REST';
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
# Please don't take this as a recommended way to do things.
# The code below is grotty, badly factored and mostly here for back
# compat..
@@ -21,18 +24,13 @@ sub _insert_self_into {
return if $req_class->isa($class);
my $req_class_meta = Moose->init_meta( for_class => $req_class );
return if $req_class_meta->does_role('Catalyst::TraitFor::Request::REST');
- if ($req_class eq 'Catalyst::Request') {
- $app->request_class($class);
- }
- else {
- my $meta = Moose::Meta::Class->create_anon_class(
- superclasses => [$req_class],
- roles => ['Catalyst::TraitFor::Request::REST'],
- cache => 1
- );
- $meta->add_method(meta => sub { $meta });
- $app->request_class($meta->name);
- }
+ my $meta = Moose::Meta::Class->create_anon_class(
+ superclasses => [$req_class],
+ roles => ['Catalyst::TraitFor::Request::REST'],
+ cache => 1
+ );
+ $meta->add_method(meta => sub { $meta });
+ $app->request_class($meta->name);
}
__PACKAGE__->meta->make_immutable;
@@ -53,11 +51,12 @@ Catalyst::Request::REST - A REST-y subclass of Catalyst::Request
=head1 DESCRIPTION
This is a subclass of C<Catalyst::Request> that applies the
-L<Catalyst::TraitFor::Request::REST> which adds a few methods to
-the request object to faciliate writing REST-y code.
+L<Catalyst::TraitFor::Request::REST> role to your request class. That trait
+adds a few methods to the request object to facilitate writing REST-y code.
-This class is only here for backwards compatibility with applications
-already subclassing this class.
+This class is only here for backwards compatibility with applications already
+subclassing this class. New code should use
+L<Catalyst::TraitFor::Request::REST> directly.
L<Catalyst::Action::REST> and L<Catalyst::Controller::REST> will arrange
for the request trait to be applied if needed.
@@ -0,0 +1,200 @@
+package Catalyst::TraitFor::Request::REST::ForBrowsers;
+use Moose::Role;
+use namespace::autoclean;
+
+with 'Catalyst::TraitFor::Request::REST';
+
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
+has _determined_real_method => (
+ is => 'rw',
+ isa => 'Bool',
+);
+
+has looks_like_browser => (
+ is => 'rw',
+ isa => 'Bool',
+ lazy => 1,
+ builder => '_build_looks_like_browser',
+ init_arg => undef,
+);
+
+# All this would be much less gross if Catalyst::Request used a builder to
+# determine the method. Then we could just wrap the builder.
+around method => sub {
+ my $orig = shift;
+ my $self = shift;
+
+ return $self->$orig(@_)
+ if @_ || $self->_determined_real_method;
+
+ my $method = $self->$orig();
+
+ my $tunneled;
+ if ( defined $method && uc $method eq 'POST' ) {
+ $tunneled = $self->param('x-tunneled-method')
+ || $self->header('x-http-method-override');
+ }
+
+ $self->$orig( defined $tunneled ? uc $tunneled : $method );
+
+ $self->_determined_real_method(1);
+
+ return $self->$orig();
+};
+
+{
+ my %HTMLTypes = map { $_ => 1 } qw(
+ text/html
+ application/xhtml+xml
+ );
+
+ sub _build_looks_like_browser {
+ my $self = shift;
+
+ my $with = $self->header('x-requested-with');
+ return 0
+ if $with && grep { $with eq $_ }
+ qw( HTTP.Request XMLHttpRequest );
+
+ if ( uc $self->method eq 'GET' ) {
+ my $forced_type = $self->param('content-type');
+ return 0
+ if $forced_type && !$HTMLTypes{$forced_type};
+ }
+
+ # IE7 does not say it accepts any form of html, but _does_
+ # accept */* (helpful ;)
+ return 1
+ if $self->accepts('*/*');
+
+ return 1
+ if grep { $self->accepts($_) } keys %HTMLTypes;
+
+ return 0
+ if @{ $self->accepted_content_types() };
+
+ # If the client did not specify any content types at all,
+ # assume they are a browser.
+ return 1;
+ }
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+Catalyst::TraitFor::Request::REST::ForBrowsers - A request trait for REST and browsers
+
+=head1 SYNOPSIS
+
+ package MyApp;
+
+ use Catalyst::TraitFor::Request::REST::ForBrowsers;
+
+
+
+=head1 DESCRIPTION
+
+Writing REST-y apps is a good thing, but if you're also trying to support web
+browsers, you're probably going to need some hackish workarounds. This module
+provides those workarounds for you.
+
+Specifically, it lets you do two things. First, it lets you "tunnel" PUT and
+DELETE requests across a POST, since most browsers do not support PUT or
+DELETE actions (as of early 2009, at least).
+
+Second, it provides a heuristic to check if the client is a web browser,
+regardless of what content types it claims to accept. The reason for this is
+that while a browser might claim to accept the "application/xml" content type,
+it's really not going to do anything useful with it, and you're best off
+giving it HTML.
+
+=head1 METHODS
+
+This class provides the following methods:
+
+=head2 $request->method
+
+This method works just like C<< Catalyst::Request->method() >> except it
+allows for tunneling of PUT and DELETE requests via a POST.
+
+Specifically, you can provide a form element named "x-tunneled-method" which
+can override the request method for a POST. This I<only> works for a POST, not
+a GET.
+
+You can also use a header named "x-http-method-override" instead (Google uses
+this header for its APIs).
+
+=head2 $request->looks_like_browser
+
+This attribute provides a heuristic to determine whether or not the request
+I<appears> to come from a browser. You can use this however you want. I
+usually use it to determine whether or not to give the client a full HTML page
+or some sort of serialized data.
+
+This is a heuristic, and like any heuristic, it is probably wrong
+sometimes. Here is how it works:
+
+=over 4
+
+=item *
+
+If the request includes a header "X-Request-With" set to either "HTTP.Request"
+or "XMLHttpRequest", this returns false. The assumption is that if you're
+doing XHR, you don't want the request treated as if it comes from a browser.
+
+=item *
+
+If the client makes a GET request with a query string parameter
+"content-type", and that type is I<not> an HTML type, it is I<not> a browser.
+
+=item *
+
+If the client provides an Accept header which includes "*/*" as an accepted
+content type, the client is a browser. Specifically, it is IE7, which submits
+an Accept header of "*/*". IE7's Accept header does not include any html types
+like "text/html".
+
+=item *
+
+If the client provides an Accept header and accepts either "text/html" or
+"application/xhtml+xml" it is a browser.
+
+=item *
+
+If it provides an Accept header of any sort, it is I<not> a browser.
+
+=item *
+
+The default is that the client is a browser.
+
+=back
+
+This all works well for my apps, but read it carefully to make sure it meets
+your expectations before using it.
+
+=head1 AUTHOR
+
+Dave Rolsky, C<< <autarch@urth.org> >>
+
+=head1 BUGS
+
+Please report any bugs or feature requests to
+C<bug-catalyst-action-rest@rt.cpan.org>, or through the web interface at
+L<http://rt.cpan.org>. We will be notified, and then you'll automatically be
+notified of progress on your bug as I make changes.
+
+=head1 COPYRIGHT & LICENSE
+
+Copyright 2008-2010 Dave Rolsky, All Rights Reserved.
+
+This program is free software; you can redistribute it and/or modify it under
+the same terms as Perl itself.
+
+=cut
@@ -3,13 +3,30 @@ use Moose::Role;
use HTTP::Headers::Util qw(split_header_words);
use namespace::autoclean;
+our $VERSION = '0.83';
+$VERSION = eval $VERSION;
+
has [qw/ data accept_only /] => ( is => 'rw' );
-sub accepted_content_types {
+has accepted_content_types => (
+ is => 'ro',
+ isa => 'ArrayRef',
+ lazy => 1,
+ builder => '_build_accepted_content_types',
+ init_arg => undef,
+);
+
+has preferred_content_type => (
+ is => 'ro',
+ isa => 'Str',
+ lazy => 1,
+ builder => '_build_preferred_content_type',
+ init_arg => undef,
+);
+
+sub _build_accepted_content_types {
my $self = shift;
- return $self->{content_types} if $self->{content_types};
-
my %types;
# First, we use the content type in the HTTP Request. It wins all.
@@ -49,11 +66,10 @@ sub accepted_content_types {
}
}
- return $self->{content_types} =
- [ sort { $types{$b} <=> $types{$a} } keys %types ];
+ [ sort { $types{$b} <=> $types{$a} } keys %types ];
}
-sub preferred_content_type { $_[0]->accepted_content_types->[0] }
+sub _build_preferred_content_type { $_[0]->accepted_content_types->[0] }
sub accepts {
my $self = shift;
@@ -80,7 +96,7 @@ Catalyst::TraitFor::Request::REST - A role to apply to Catalyst::Request giving
=head1 DESCRIPTION
This is a L<Moose::Role> applied to L<Catalyst::Request> that adds a few
-methods to the request object to faciliate writing REST-y code.
+methods to the request object to facilitate writing REST-y code.
Currently, these methods are all related to the content types accepted by
the client.
@@ -1,6 +1,6 @@
use strict;
use warnings;
-use Test::More tests => 16;
+use Test::More;
use Data::Serializer;
use FindBin;
@@ -88,4 +88,14 @@ SKIP: {
is( $res->header('Content-type'), 'text/x-data-dumper', '... with expected content-type')
}
-1;
+# Make sure that the default content type you specify really gets used.
+{
+ my $req = $t->get(url => '/override/test');
+ $req->remove_header('Content-Type');
+ my $res = request($req);
+ ok( $res->is_success, 'GET the serialized request succeeded' );
+ is( $res->content, "--- \nlou: is my cat\n", "Request returned proper data");
+}
+
+done_testing;
+
@@ -1,204 +0,0 @@
-use strict;
-use warnings;
-use Test::More;
-use FindBin;
-use lib ( "$FindBin::Bin/../lib", "$FindBin::Bin/../t/lib" );
-
-use Catalyst::Request::REST;
-use HTTP::Headers;
-
-{
- my $request = Catalyst::Request::REST->new;
- $request->{_context} = 'MockContext';
- $request->headers( HTTP::Headers->new );
- $request->parameters( {} );
- $request->method('GET');
- $request->content_type('text/foobar');
-
- is_deeply( $request->accepted_content_types, [ 'text/foobar' ],
- 'content-type set in request headers is found' );
- is( $request->preferred_content_type, 'text/foobar',
- 'preferred content type is text/foobar' );
- ok( ! $request->accept_only, 'accept_only is false' );
- ok( $request->accepts('text/foobar'), 'accepts text/foobar' );
- ok( ! $request->accepts('text/html'), 'does not accept text/html' );
-}
-
-{
- my $request = Catalyst::Request::REST->new;
- $request->{_context} = 'MockContext';
- $request->headers( HTTP::Headers->new );
- $request->parameters( { 'content-type' => 'text/fudge' } );
- $request->method('GET');
- $request->content_type('text/foobar');
-
- is_deeply( $request->accepted_content_types, [ 'text/foobar', 'text/fudge' ],
- 'content-type set in request headers and type in parameters is found' );
- is( $request->preferred_content_type, 'text/foobar',
- 'preferred content type is text/foobar' );
- ok( ! $request->accept_only, 'accept_only is false' );
- ok( $request->accepts('text/foobar'), 'accepts text/foobar' );
- ok( $request->accepts('text/fudge'), 'accepts text/fudge' );
- ok( ! $request->accepts('text/html'), 'does not accept text/html' );
-}
-
-{
- my $request = Catalyst::Request::REST->new;
- $request->{_context} = 'MockContext';
- $request->headers( HTTP::Headers->new );
- $request->parameters( { 'content-type' => 'text/fudge' } );
- $request->method('POST');
- $request->content_type('text/foobar');
-
- ok( ! $request->accepts('text/fudge'), 'content type in parameters is ignored for POST' );
-}
-
-{
- my $request = Catalyst::Request::REST->new;
- $request->{_context} = 'MockContext';
- $request->headers( HTTP::Headers->new );
- $request->parameters( {} );
- $request->method('GET');
- $request->headers->header(
- 'Accept' =>
- # From Firefox 2.0 when it requests an html page
- 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
- );
-
- is_deeply( $request->accepted_content_types,
- [ qw( text/xml application/xml application/xhtml+xml
- image/png
- text/html
- text/plain
- */*
- ) ],
- 'accept header is parsed properly' );
- is( $request->preferred_content_type, 'text/xml',
- 'preferred content type is text/xml' );
- ok( $request->accept_only, 'accept_only is true' );
- ok( $request->accepts('text/html'), 'accepts text/html' );
- ok( $request->accepts('image/png'), 'accepts image/png' );
- ok( ! $request->accepts('image/svg'), 'does not accept image/svg' );
-}
-
-{
- my $request = Catalyst::Request::REST->new;
- $request->{_context} = 'MockContext';
- $request->headers( HTTP::Headers->new );
- $request->parameters( {} );
- $request->method('GET');
- $request->content_type('application/json');
- $request->headers->header(
- 'Accept' =>
- # From Firefox 2.0 when it requests an html page
- 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
- );
-
- is_deeply( $request->accepted_content_types,
- [ qw( application/json
- text/xml application/xml application/xhtml+xml
- image/png
- text/html
- text/plain
- */*
- ) ],
- 'accept header is parsed properly, and content-type header has precedence over accept' );
- ok( ! $request->accept_only, 'accept_only is false' );
-}
-
-{
- my $request = Catalyst::Request::REST->new;
- $request->{_context} = 'MockContext';
- $request->headers( HTTP::Headers->new );
- $request->parameters( {} );
- $request->method('GET');
- $request->content_type('application/json');
- $request->headers->header(
- 'Accept' =>
- # From Firefox 2.0 when it requests an html page
- 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
- );
-
- is_deeply( $request->accepted_content_types,
- [ qw( application/json
- text/xml application/xml application/xhtml+xml
- image/png
- text/html
- text/plain
- */*
- ) ],
- 'accept header is parsed properly, and content-type header has precedence over accept' );
- ok( ! $request->accept_only, 'accept_only is false' );
-}
-
-{
- my $request = Catalyst::Request::REST->new;
- $request->{_context} = 'MockContext';
- $request->headers( HTTP::Headers->new );
- $request->parameters( {} );
- $request->method('GET');
- $request->content_type('text/x-json');
- $request->headers->header(
- 'Accept' => 'text/plain,text/x-json',
- );
-
- is_deeply( $request->accepted_content_types,
- [ qw( text/x-json
- text/plain
- ) ],
- 'each type appears only once' );
-}
-
-{
- my $request = Catalyst::Request::REST->new;
- $request->{_context} = 'MockContext';
- $request->headers( HTTP::Headers->new );
- $request->parameters( {} );
- $request->method('GET');
- $request->content_type('application/json');
- $request->headers->header(
- 'Accept' => 'text/plain,application/json',
- );
-
- is_deeply( $request->accepted_content_types,
- [ qw( application/json
- text/plain
- ) ],
- 'each type appears only once' );
-}
-
-{
- local %ENV=%ENV;
- $ENV{CATALYST_DEBUG} = 0;
- my $test = 'Test::Catalyst::Action::REST';
- use_ok $test;
- is($test->request_class, 'Catalyst::Request::REST',
- 'Request::REST took over for Request');
-
- my $meta = Moose::Meta::Class->create_anon_class(
- superclasses => ['Catalyst::Request'],
- );
- $meta->add_method('__random_method' => sub { 42 });
-
- $test->request_class($meta->name);
- # FIXME - setup_finished(0) is evil!
- eval { $test->setup_finished(0); $test->setup };
- ok !$@, 'Can setup again';
- isnt $test->request_class, $meta->name, 'Different request class';
- ok $test->request_class->can('__random_method'), 'Is right class';
- ok $test->request_class->can('data'), 'Also smells like REST subclass';
-
- {
- package My::Request;
- use base 'Catalyst::Request::REST';
- }
- $test->request_class('My::Request');
- eval { $test->setup_finished(0); $test->setup };
- is $@, '', 'no error from Request::REST subclass';
-}
-
-done_testing;
-
-package MockContext;
-
-sub prepare_body { }
@@ -0,0 +1,200 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+use Catalyst::Request;
+use Catalyst::Request::REST::ForBrowsers;
+use Catalyst::TraitFor::Request::REST::ForBrowsers;
+use Moose::Meta::Class;
+use HTTP::Headers;
+
+my $anon_class = Moose::Meta::Class->create_anon_class(
+ superclasses => ['Catalyst::Request'],
+ roles => ['Catalyst::TraitFor::Request::REST::ForBrowsers'],
+ cache => 1,
+)->name;
+
+# We run the tests twice to make sure Catalyst::Request::REST::ForBrowsers is
+# 100% back-compatible.
+for my $class ( $anon_class, 'Catalyst::Request::REST::ForBrowsers' ) {
+ {
+ for my $method (qw( GET POST PUT DELETE )) {
+ my $req = $class->new();
+ $req->method($method);
+ $req->{_context} = 'MockContext';
+ $req->parameters( {} );
+
+ is(
+ $req->method(), $method,
+ "$method - not tunneled"
+ );
+ }
+ }
+
+ {
+ for my $method (qw( PUT DELETE )) {
+ my $req = $class->new();
+ $req->method('POST');
+ $req->{_context} = 'MockContext';
+ $req->parameters( { 'x-tunneled-method' => $method } );
+
+ is(
+ $req->method(), $method,
+ "$method - tunneled with x-tunneled-method param"
+ );
+ }
+ }
+
+ {
+ for my $method (qw( PUT DELETE )) {
+ my $req = $class->new();
+ $req->method('POST');
+ $req->{_context} = 'MockContext';
+ $req->header( 'x-http-method-override' => $method );
+
+ is(
+ $req->method(), $method,
+ "$method - tunneled with x-http-method-override header"
+ );
+ }
+ }
+
+ {
+ for my $method (qw( PUT DELETE )) {
+ my $req = $class->new();
+ $req->method('GET');
+ $req->{_context} = 'MockContext';
+ $req->parameters( { 'x-tunneled-method' => $method } );
+
+ is(
+ $req->method(), 'GET',
+ 'x-tunneled-method is ignore with a GET'
+ );
+ }
+ }
+
+ {
+ my $req = $class->new();
+ $req->{_context} = 'MockContext';
+ $req->method('GET');
+ $req->parameters( {} );
+ $req->headers( HTTP::Headers->new() );
+
+ ok(
+ $req->looks_like_browser(),
+ 'default is a browser'
+ );
+ }
+
+ {
+ for my $with (qw( HTTP.Request XMLHttpRequest )) {
+ my $req = $class->new();
+ $req->{_context} = 'MockContext';
+ $req->headers(
+ HTTP::Headers->new( 'X-Requested-With' => $with ) );
+
+ ok(
+ !$req->looks_like_browser(),
+ "not a browser - X-Request-With = $with"
+ );
+ }
+ }
+
+ {
+ my $req = $class->new();
+ $req->{_context} = 'MockContext';
+ $req->method('GET');
+ $req->parameters( { 'content-type' => 'text/json' } );
+ $req->headers( HTTP::Headers->new() );
+
+ ok(
+ !$req->looks_like_browser(),
+ 'forced non-HTML content-type is not a browser'
+ );
+ }
+
+ {
+ my $req = $class->new();
+ $req->{_context} = 'MockContext';
+ $req->method('GET');
+ $req->parameters( { 'content-type' => 'text/html' } );
+ $req->headers( HTTP::Headers->new() );
+
+ ok(
+ $req->looks_like_browser(),
+ 'forced HTML content-type is not a browser'
+ );
+ }
+
+ {
+ my $req = $class->new();
+ $req->{_context} = 'MockContext';
+ $req->method('GET');
+ $req->parameters( {} );
+ $req->headers(
+ HTTP::Headers->new( 'Accept' => 'text/xml; q=0.4, */*; q=0.2' ) );
+
+ ok(
+ $req->looks_like_browser(),
+ 'if it accepts */* it is a browser'
+ );
+ }
+
+ {
+ my $req = $class->new();
+ $req->{_context} = 'MockContext';
+ $req->method('GET');
+ $req->parameters( {} );
+ $req->headers(
+ HTTP::Headers->new(
+ 'Accept' => 'text/html; q=0.4, text/xml; q=0.2'
+ )
+ );
+
+ ok(
+ $req->looks_like_browser(),
+ 'if it accepts text/html it is a browser'
+ );
+ }
+
+ {
+ my $req = $class->new();
+ $req->{_context} = 'MockContext';
+ $req->method('GET');
+ $req->parameters( {} );
+ $req->headers(
+ HTTP::Headers->new(
+ 'Accept' => 'application/xhtml+xml; q=0.4, text/xml; q=0.2'
+ )
+ );
+
+ ok(
+ $req->looks_like_browser(),
+ 'if it accepts application/xhtml+xml it is a browser'
+ );
+ }
+
+ {
+ my $req = $class->new();
+ $req->{_context} = 'MockContext';
+ $req->method('GET');
+ $req->parameters( {} );
+ $req->headers(
+ HTTP::Headers->new(
+ 'Accept' => 'text/json; q=0.4, text/xml; q=0.2'
+ )
+ );
+
+ ok(
+ !$req->looks_like_browser(),
+ 'provided an Accept header but does not accept html, is not a browser'
+ );
+ }
+}
+
+done_testing;
+
+package MockContext;
+
+sub prepare_body { }
@@ -0,0 +1,215 @@
+use strict;
+use warnings;
+use Test::More;
+use FindBin;
+use lib ( "$FindBin::Bin/../lib", "$FindBin::Bin/../t/lib" );
+
+use Catalyst::Request::REST;
+use Catalyst::TraitFor::Request::REST;
+use Moose::Meta::Class;
+use HTTP::Headers;
+
+my $anon_class = Moose::Meta::Class->create_anon_class(
+ superclasses => ['Catalyst::Request'],
+ roles => ['Catalyst::TraitFor::Request::REST::ForBrowsers'],
+ cache => 1,
+)->name;
+
+for my $class ( $anon_class, 'Catalyst::Request::REST' ) {
+ {
+ my $request = Catalyst::Request::REST->new;
+ $request->{_context} = 'MockContext';
+ $request->headers( HTTP::Headers->new );
+ $request->parameters( {} );
+ $request->method('GET');
+ $request->content_type('text/foobar');
+
+ is_deeply( $request->accepted_content_types, [ 'text/foobar' ],
+ 'content-type set in request headers is found' );
+ is( $request->preferred_content_type, 'text/foobar',
+ 'preferred content type is text/foobar' );
+ ok( ! $request->accept_only, 'accept_only is false' );
+ ok( $request->accepts('text/foobar'), 'accepts text/foobar' );
+ ok( ! $request->accepts('text/html'), 'does not accept text/html' );
+ }
+
+ {
+ my $request = Catalyst::Request::REST->new;
+ $request->{_context} = 'MockContext';
+ $request->headers( HTTP::Headers->new );
+ $request->parameters( { 'content-type' => 'text/fudge' } );
+ $request->method('GET');
+ $request->content_type('text/foobar');
+
+ is_deeply( $request->accepted_content_types, [ 'text/foobar', 'text/fudge' ],
+ 'content-type set in request headers and type in parameters is found' );
+ is( $request->preferred_content_type, 'text/foobar',
+ 'preferred content type is text/foobar' );
+ ok( ! $request->accept_only, 'accept_only is false' );
+ ok( $request->accepts('text/foobar'), 'accepts text/foobar' );
+ ok( $request->accepts('text/fudge'), 'accepts text/fudge' );
+ ok( ! $request->accepts('text/html'), 'does not accept text/html' );
+ }
+
+ {
+ my $request = Catalyst::Request::REST->new;
+ $request->{_context} = 'MockContext';
+ $request->headers( HTTP::Headers->new );
+ $request->parameters( { 'content-type' => 'text/fudge' } );
+ $request->method('POST');
+ $request->content_type('text/foobar');
+
+ ok( ! $request->accepts('text/fudge'), 'content type in parameters is ignored for POST' );
+ }
+
+ {
+ my $request = Catalyst::Request::REST->new;
+ $request->{_context} = 'MockContext';
+ $request->headers( HTTP::Headers->new );
+ $request->parameters( {} );
+ $request->method('GET');
+ $request->headers->header(
+ 'Accept' =>
+ # From Firefox 2.0 when it requests an html page
+ 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
+ );
+
+ is_deeply( $request->accepted_content_types,
+ [ qw( text/xml application/xml application/xhtml+xml
+ image/png
+ text/html
+ text/plain
+ */*
+ ) ],
+ 'accept header is parsed properly' );
+ is( $request->preferred_content_type, 'text/xml',
+ 'preferred content type is text/xml' );
+ ok( $request->accept_only, 'accept_only is true' );
+ ok( $request->accepts('text/html'), 'accepts text/html' );
+ ok( $request->accepts('image/png'), 'accepts image/png' );
+ ok( ! $request->accepts('image/svg'), 'does not accept image/svg' );
+ }
+
+ {
+ my $request = Catalyst::Request::REST->new;
+ $request->{_context} = 'MockContext';
+ $request->headers( HTTP::Headers->new );
+ $request->parameters( {} );
+ $request->method('GET');
+ $request->content_type('application/json');
+ $request->headers->header(
+ 'Accept' =>
+ # From Firefox 2.0 when it requests an html page
+ 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
+ );
+
+ is_deeply( $request->accepted_content_types,
+ [ qw( application/json
+ text/xml application/xml application/xhtml+xml
+ image/png
+ text/html
+ text/plain
+ */*
+ ) ],
+ 'accept header is parsed properly, and content-type header has precedence over accept' );
+ ok( ! $request->accept_only, 'accept_only is false' );
+ }
+
+ {
+ my $request = Catalyst::Request::REST->new;
+ $request->{_context} = 'MockContext';
+ $request->headers( HTTP::Headers->new );
+ $request->parameters( {} );
+ $request->method('GET');
+ $request->content_type('application/json');
+ $request->headers->header(
+ 'Accept' =>
+ # From Firefox 2.0 when it requests an html page
+ 'text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5',
+ );
+
+ is_deeply( $request->accepted_content_types,
+ [ qw( application/json
+ text/xml application/xml application/xhtml+xml
+ image/png
+ text/html
+ text/plain
+ */*
+ ) ],
+ 'accept header is parsed properly, and content-type header has precedence over accept' );
+ ok( ! $request->accept_only, 'accept_only is false' );
+ }
+
+ {
+ my $request = Catalyst::Request::REST->new;
+ $request->{_context} = 'MockContext';
+ $request->headers( HTTP::Headers->new );
+ $request->parameters( {} );
+ $request->method('GET');
+ $request->content_type('text/x-json');
+ $request->headers->header(
+ 'Accept' => 'text/plain,text/x-json',
+ );
+
+ is_deeply( $request->accepted_content_types,
+ [ qw( text/x-json
+ text/plain
+ ) ],
+ 'each type appears only once' );
+ }
+
+ {
+ my $request = Catalyst::Request::REST->new;
+ $request->{_context} = 'MockContext';
+ $request->headers( HTTP::Headers->new );
+ $request->parameters( {} );
+ $request->method('GET');
+ $request->content_type('application/json');
+ $request->headers->header(
+ 'Accept' => 'text/plain,application/json',
+ );
+
+ is_deeply( $request->accepted_content_types,
+ [ qw( application/json
+ text/plain
+ ) ],
+ 'each type appears only once' );
+ }
+}
+
+{
+ local %ENV=%ENV;
+ $ENV{CATALYST_DEBUG} = 0;
+ my $test = 'Test::Catalyst::Action::REST';
+ use_ok $test;
+ ok($test->request_class->meta->does_role('Catalyst::TraitFor::Request::REST'),
+ 'request class does REST role');
+
+ my $meta = Moose::Meta::Class->create_anon_class(
+ superclasses => ['Catalyst::Request'],
+ );
+ $meta->add_method('__random_method' => sub { 42 });
+
+ $test->request_class($meta->name);
+ # FIXME - setup_finished(0) is evil!
+ eval { $test->setup_finished(0); $test->setup };
+ ok !$@, 'Can setup again';
+ isnt $test->request_class, $meta->name, 'Different request class';
+ ok $test->request_class->can('__random_method'), 'Is right class';
+ ok($test->request_class->meta->does_role('Catalyst::TraitFor::Request::REST'),
+ 'request class still does REST role');
+
+ {
+ package My::Request;
+ use base 'Catalyst::Request::REST';
+ }
+ $test->request_class('My::Request');
+ eval { $test->setup_finished(0); $test->setup };
+ is $@, '', 'no error from Request::REST subclass';
+}
+
+done_testing;
+
+package MockContext;
+
+sub prepare_body { }
@@ -0,0 +1,22 @@
+package Test::Catalyst::Action::REST::Controller::Override;
+
+use Moose;
+use namespace::autoclean;
+
+BEGIN { extends 'Catalyst::Controller' }
+
+__PACKAGE__->config(
+ 'default' => 'application/json',
+ 'map' => {
+ 'application/json' => 'YAML', # Yes, this is deliberate!
+ },
+);
+
+sub test :Local :ActionClass('Serialize') {
+ my ( $self, $c ) = @_;
+ $c->stash->{'rest'} = {
+ lou => 'is my cat',
+ };
+}
+
+1;
@@ -7,6 +7,7 @@ use Catalyst::Runtime '5.70';
use Catalyst;
use FindBin;
+use Test::Catalyst::Log;
__PACKAGE__->config(
name => 'Test::Catalyst::Action::REST',
@@ -16,5 +17,6 @@ __PACKAGE__->config(
},
);
__PACKAGE__->setup;
+__PACKAGE__->log( Test::Catalyst::Log->new );
1;
@@ -0,0 +1,21 @@
+package Test::Catalyst::Log;
+
+use strict;
+use warnings;
+
+sub new {
+ bless {}, __PACKAGE__;
+}
+
+sub is_debug { 0 }
+sub debug { }
+sub is_info { 0 }
+sub info { }
+sub is_warn { 0 }
+sub warn : method { }
+sub is_error { 0 }
+sub error { }
+sub is_fatal { 0 }
+sub fatal { }
+
+1;
@@ -27,6 +27,9 @@ __PACKAGE__->config(
'text/view' => [ 'View', 'Simple' ],
'text/explodingview' => [ 'View', 'Awful' ],
'text/broken' => 'Broken',
+ 'text/javascript', => 'JSONP',
+ 'application/x-javascript' => 'JSONP',
+ 'application/javascript' => 'JSONP',
},
);
@@ -10,12 +10,14 @@ use namespace::autoclean;
use Catalyst::Runtime '5.70';
use Catalyst;
+use Test::Catalyst::Log;
__PACKAGE__->config(
name => 'Test::Serialize',
);
__PACKAGE__->setup;
+__PACKAGE__->log( Test::Catalyst::Log->new );
1;
@@ -1,9 +0,0 @@
-use strict;
-use warnings;
-use Test::More;
-
-eval "use Test::Pod 1.14";
-plan skip_all => 'Test::Pod 1.14 required' if $@;
-plan skip_all => 'set TEST_POD to enable this test' unless $ENV{TEST_POD};
-
-all_pod_files_ok();
@@ -0,0 +1,64 @@
+use strict;
+use warnings;
+
+use Test::More;
+
+eval "use Test::Spelling";
+plan skip_all => "Test::Spelling required for testing POD coverage"
+ if $@;
+
+my @stopwords;
+for (<DATA>) {
+ chomp;
+ push @stopwords, $_
+ unless /\A (?: \# | \s* \z)/msx; # skip comments, whitespace
+}
+
+add_stopwords(@stopwords);
+set_spell_cmd('aspell list -l en');
+
+# This prevents a weird segfault from the aspell command - see
+# https://bugs.launchpad.net/ubuntu/+source/aspell/+bug/71322
+local $ENV{LC_ALL} = 'C';
+all_pod_files_spelling_ok();
+
+__DATA__
+APIs
+ActionClass
+Daisuke
+Daisuke
+Deserialize
+Deserializer
+Deserializing
+Doran
+Goulah
+JSON
+Laco
+Maki
+Maki
+Marchex
+Pearcey
+RESTful
+RESTful
+SERIALIZERS
+TT
+Wikipedia
+XHR
+XMLHttpRequest
+YAML
+conf
+deserialize
+deserialized
+deserializing
+fREW
+html
+http
+javascript
+jrockway
+mst
+namespace
+plugins
+request's
+serializer
+thusly
+wildcard
@@ -0,0 +1,7 @@
+use strict;
+use warnings;
+use Test::More;
+
+use Test::Pod 1.14;
+
+all_pod_files_ok();
@@ -0,0 +1,23 @@
+use strict;
+use warnings;
+
+use File::Find::Rule;
+use Module::Info;
+
+use Test::More qw( no_plan );
+
+my %versions;
+for my $pm_file ( File::Find::Rule->file->name( qr/\.pm$/ )->in('lib' ) ) {
+ my $mod = Module::Info->new_from_file($pm_file);
+
+ ( my $stripped_file = $pm_file ) =~ s{^lib/}{};
+
+ $versions{$stripped_file} = $mod->version;
+}
+
+my $moose_ver = $versions{'Catalyst/Action/REST.pm'};
+
+for my $module ( grep { $_ ne 'Catalyst/Action/REST.pm' } sort keys %versions ) {
+ is( $versions{$module}, $moose_ver,
+ "version for $module is the same as in Catalyst/Action/REST.pm" );
+}