view release on metacpan or search on metacpan
- Fixed an issue where YAML::Syck versions 0.92 require $c->request->body to
be stringified
- 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.
- 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)
0.41 2007-05-24 14:01:06 America/Los_Angeles (adam)
- Moved a bogus $self->class to $c->component($self->class)
0.40 2007-03-09 14:13:29 America/Los_Angeles (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)
0.31 2006-12-06 00:45:02 America/Los_Angeles (adam)
- Fixed a bug where we would report a blank content-type negotiation.
- Added Data::Dump as a dependency.
- Made the YAML::HTML view automatically append content-type=text/html on
the resulting URLs.
0.30 2006-12-03 12:24:16 America/Los_Angeles (adam)
- 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.
Catalyst::Action::Serialize, Catalyst::Action::Deserialize
TROUBLESHOOTING
Q: I'm getting a "415 Unsupported Media Type" error. What gives?!
A: Most likely, you haven't set Content-type equal to
"application/json", or one of the accepted return formats. You can do
this by setting it in your query accepted return formats. You can do
this by setting it in your query string thusly:
?content-type=application%2Fjson (where %2F == / uri escaped).
NOTE Apache will refuse %2F unless configured otherwise. Make sure
AllowEncodedSlashes On is in your httpd.conf file in order for this
to run smoothly.
AUTHOR
Adam Jacob <adam@stalecoffee.org>, with lots of help from mst and
jrockway
lib/Catalyst/Action/Deserialize.pm view on Meta::CPAN
'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ],
},
);
sub begin :ActionClass('Deserialize') {}
=head1 DESCRIPTION
This action will deserialize HTTP POST, PUT, OPTIONS and DELETE requests.
It assumes that the body of the HTTP Request is a serialized object.
The serializer is selected by introspecting the requests content-type
header.
If you want deserialize any other HTTP method besides POST, PUT,
OPTIONS and DELETE you can do this by setting the
C<< deserialize_http_methods >> list via C<< action_args >>.
Just modify the config in your controller and define a list of HTTP
methods the deserialization should happen for:
__PACKAGE__->config(
action_args => {
'*' => {
deserialize_http_methods => [qw(POST PUT OPTIONS DELETE GET)]
}
}
);
See also L<Catalyst::Controller/action_args>.
The specifics of deserializing each content-type is implemented as
a plugin to L<Catalyst::Action::Deserialize>. You can see a list
of currently implemented plugins in L<Catalyst::Controller::REST>.
The results of your Deserializing will wind up in $c->req->data.
This is done through the magic of L<Catalyst::Request::REST>.
While it is common for this Action to be called globally as a
C<begin> method, there is nothing stopping you from using it on a
single routine:
lib/Catalyst/Action/DeserializeMultiPart.pm view on Meta::CPAN
sub begin :ActionClass('DeserializeMultiPart') DeserializePart('REST') {}
=head1 DESCRIPTION
This action will deserialize multipart HTTP POST, PUT, OPTIONS and DELETE
requests. It is a simple extension of L<Catalyst::Action::Deserialize>
with the exception that rather than using the entire request body (which
may contain multiple sections), it will look for a single part in the request
body named according to the C<DeserializePart> attribute on that action
(defaulting to C<REST>). If a part is found under that name, it then
proceeds to deserialize the request as normal based on the content-type
of that individual part. If no such part is found, the request would
be processed as if no data was sent.
This module's code will only come into play if the following conditions are met:
=over 4
=item * The C<Content-type> of the request is C<multipart/*>
=item * The request body (as returned by C<$c->request->body> is not defined
lib/Catalyst/Action/DeserializeMultiPart.pm view on Meta::CPAN
to map to L<HTTP::Body::MultiPart>. This module makes the assumption
that you would like to have all C<multipart/mixed> requests parsed by
L<HTTP::Body::MultiPart> module. This is done by a package variable
inside L<HTTP::Body>: C<$HTTP::Body::Types> (a HASH ref).
B<WARNING:> As this module modifies the behaviour of HTTP::Body globally,
adding it to an application can have unintended consequences as multipart
bodies will be parsed differently from before.
Feel free to
add other content-types to this hash if needed or if you would prefer
that C<multipart/mixed> NOT be added to this hash, simply delete it
after loading this module.
# in your controller
use Catalyst::Action::DeserializeMultiPart;
delete $HTTP::Body::Types->{'multipart/mixed'};
$HTTP::Body::Types->{'multipart/my-crazy-content-type'} = 'HTTP::Body::MultiPart';
=head1 SEE ALSO
This is a simple sub-class of L<Catalyst::Action::Deserialize>.
=head1 AUTHORS
See L<Catalyst::Action::REST> for authors.
=head1 LICENSE
lib/Catalyst/Action/REST.pm view on Meta::CPAN
=head1 TROUBLESHOOTING
=over 4
=item Q: I'm getting a "415 Unsupported Media Type" error. What gives?!
A: Most likely, you haven't set Content-type equal to "application/json", or
one of the accepted return formats. You can do this by setting it in your query
accepted return formats. You can do this by setting it in your query string
thusly: C<< ?content-type=application%2Fjson (where %2F == / uri escaped). >>
B<NOTE> Apache will refuse %2F unless configured otherwise.
Make sure C<AllowEncodedSlashes On> is in your httpd.conf file in order
for this to run smoothly.
=back
=head1 AUTHOR
Adam Jacob E<lt>adam@stalecoffee.orgE<gt>, with lots of help from mst and jrockway
lib/Catalyst/Action/Serialize.pm view on Meta::CPAN
return 1 if $c->response->status =~ /^(?:3\d\d)$/ && ! defined $c->stash->{$stash_key};
my ( $sclass, $sarg, $content_type ) =
$self->_load_content_plugins( "Catalyst::Action::Serialize",
$controller, $c );
unless ( defined($sclass) ) {
if ( defined($content_type) ) {
$c->log->info("Could not find a serializer for $content_type");
} else {
$c->log->info(
"Could not find a serializer for an empty content-type");
}
return 1;
}
$c->log->debug(
"Serializing with $sclass" . ( $sarg ? " [$sarg]" : '' ) ) if $c->debug;
$self->_encoders->{$sclass} ||= $sclass->new;
my $sobj = $self->_encoders->{$sclass};
my $rc;
lib/Catalyst/Action/Serialize.pm view on Meta::CPAN
'text/x-yaml' => 'YAML',
'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ],
}
);
sub end :ActionClass('Serialize') {}
=head1 DESCRIPTION
This action will serialize the body of an HTTP Response. The serializer is
selected by introspecting the HTTP Requests content-type header.
It requires that your Catalyst controller is properly configured to set up the
mapping between Content Type's and Serialization classes.
The specifics of serializing each content-type is implemented as a plugin to
L<Catalyst::Action::Serialize>.
Typically, you would use this ActionClass on your C<end> method. However,
nothing is stopping you from choosing specific methods to Serialize:
sub foo :Local :ActionClass('Serialize') {
.. populate stash with data ..
}
When you use this module, the request class will be changed to
lib/Catalyst/Action/Serialize/YAML/HTML.pm view on Meta::CPAN
my $output = "<html>";
$output .= "<title>" . $app . "</title>";
$output .= "<body><pre>";
my $text = HTML::Entities::encode(Dump($c->stash->{$stash_key}));
# Straight from URI::Find
my $finder = URI::Find->new(
sub {
my($uri, $orig_uri) = @_;
my $newuri;
if ($uri =~ /\?/) {
$newuri = $uri . "&content-type=text/html";
} else {
$newuri = $uri . "?content-type=text/html";
}
return qq|<a href="$newuri">$orig_uri</a>|;
});
$finder->find(\$text);
$output .= $text;
$output .= "</pre>";
$output .= "</body>";
$output .= "</html>";
$c->response->output( $output );
return 1;
lib/Catalyst/Action/SerializeBase.pm view on Meta::CPAN
# Load the Serialize Classes
unless ( defined( $self->_serialize_plugins ) ) {
my @plugins;
my $mpo =
Module::Pluggable::Object->new( 'search_path' => [$search_path], );
@plugins = $mpo->plugins;
$self->_serialize_plugins( \@plugins );
}
# Finally, we load the class. If you have a default serializer,
# and we still don't have a content-type that exists in the map,
# we'll use it.
my $sclass = $search_path . "::";
my $sarg;
my $map;
my $compliance_mode;
my $default;
my $config;
if ( exists $controller->{'serialize'} ) {
lib/Catalyst/Action/SerializeBase.pm view on Meta::CPAN
# then the default
push @accepted_types, $default if $default;
# pick the best match that we have a serializer mapping for
my ($content_type) = grep { $map->{$_} } @accepted_types;
return $self->unsupported_media_type($c, $content_type)
if not $content_type;
# carp about old text/x-json
if ($content_type eq 'text/x-json') {
$c->log->info('Using deprecated text/x-json content-type.');
$c->log->info('Use application/json instead!');
}
if ( exists( $map->{$content_type} ) ) {
my $mc;
if ( ref( $map->{$content_type} ) eq "ARRAY" ) {
$mc = $map->{$content_type}->[0];
$sarg = $map->{$content_type}->[1];
} else {
$mc = $map->{$content_type};
lib/Catalyst/Action/SerializeBase.pm view on Meta::CPAN
__PACKAGE__->meta->make_immutable;
1;
=head1 NAME
Catalyst::Action::SerializeBase - Base class for Catalyst::Action::Serialize and Catlayst::Action::Deserialize.
=head1 DESCRIPTION
This module implements the plugin loading and content-type negotiating
code for L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize>.
=head1 SEE ALSO
L<Catalyst::Action::Serialize>, L<Catalyst::Action::Deserialize>,
L<Catalyst::Controller::REST>,
=head1 AUTHORS
See L<Catalyst::Action::REST> for authors.
lib/Catalyst/Controller/REST.pm view on Meta::CPAN
Any unimplemented HTTP methods will be met with a "405 Method Not Allowed"
response, automatically containing the proper list of available methods. You
can override this behavior through implementing a custom
C<thing_not_implemented> method.
If you do not provide an OPTIONS handler, we will respond to any OPTIONS
requests with a "200 OK", populating the Allowed header automatically.
Any data included in C<< $c->stash->{'rest'} >> will be serialized for you.
The serialization format will be selected based on the content-type
of the incoming request. It is probably easier to use the L<STATUS HELPERS>,
which are described below.
"The HTTP POST, PUT, and OPTIONS methods will all automatically
L<deserialize|Catalyst::Action::Deserialize> the contents of
C<< $c->request->body >> into the C<< $c->request->data >> hashref", based on
the request's C<Content-type> header. A list of understood serialization
formats is L<below|/AVAILABLE SERIALIZERS>.
If we do not have (or cannot run) a serializer for a given content-type, a 415
"Unsupported Media Type" error is generated.
To make your Controller RESTful, simply have it
BEGIN { extends 'Catalyst::Controller::REST' }
=head1 CONFIGURATION
See L<Catalyst::Action::Serialize/CONFIGURATION>. Note that the C<serialize>
key has been deprecated.
=head1 SERIALIZATION
Catalyst::Controller::REST will automatically serialize your
responses, and deserialize any POST, PUT or OPTIONS requests. It evaluates
which serializer to use by mapping a content-type to a Serialization module.
We select the content-type based on:
=over
=item B<The Content-Type Header>
If the incoming HTTP Request had a Content-Type header set, we will use it.
=item B<The content-type Query Parameter>
If this is a GET request, you can supply a content-type query parameter.
=item B<Evaluating the Accept Header>
Finally, if the client provided an Accept header, we will evaluate
it and use the best-ranked choice.
=back
=head1 AVAILABLE SERIALIZERS
lib/Catalyst/Controller/REST.pm view on Meta::CPAN
the deserialization. The C<serialize> callback is passed the data
structure that needs to be serialized and must return a string suitable
for returning in the HTTP response. In addition to receiving the scalar
to act on, both callbacks are passed the controller object and the context
(i.e. C<$c>) as the second and third arguments.
=back
By default, L<Catalyst::Controller::REST> will return a
C<415 Unsupported Media Type> response if an attempt to use an unsupported
content-type is made. You can ensure that something is always returned by
setting the C<default> config option:
__PACKAGE__->config(default => 'text/x-yaml');
would make it always fall back to the serializer plugin defined for
C<text/x-yaml>.
=head1 CUSTOM SERIALIZERS
Implementing new Serialization formats is easy! Contributions
are most welcome! If you would like to implement a custom serializer,
you should create two new modules in the L<Catalyst::Action::Serialize>
and L<Catalyst::Action::Deserialize> namespace. Then assign your new
class to the content-type's you want, and you're done.
See L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize>
for more information.
=head1 STATUS HELPERS
Since so much of REST is in using HTTP, we provide these Status Helpers.
Using them will ensure that you are responding with the proper codes,
headers, and entities.
lib/Catalyst/TraitFor/Request/REST.pm view on Meta::CPAN
my $self = shift;
my %types;
# First, we use the content type in the HTTP Request. It wins all.
# But only examine it if we're not in compliance mode or if we're
# in deserializing mode
$types{ $self->content_type } = 3
if $self->content_type && $self->content_type_allowed();
# Seems backwards, but users are used to adding &content-type= to the uri to
# define what content type they want to recieve back, in the equivalent Accept
# header. Let the users do what they're used to, it's outside the RFC
# specifications anyhow.
if ($self->method eq "GET" && $self->param('content-type') && $self->accept_allowed()) {
$types{ $self->param('content-type') } = 2;
}
# Third, we parse the Accept header, and see if the client
# takes a format we understand.
# But only examine it if we're not in compliance mode or if we're
# in serializing mode
#
# This is taken from chansen's Apache2::UploadProgress.
if ( $self->header('Accept') && $self->accept_allowed() ) {
$self->accept_only(1) unless keys %types;
lib/Catalyst/TraitFor/Request/REST.pm view on Meta::CPAN
client.
The list of types is created by looking at the following sources:
=over 8
=item * Content-type header
If this exists, this will always be the first type in the list.
=item * content-type parameter
If the request is a GET request and there is a "content-type"
parameter in the query string, this will come before any types in the
Accept header.
=item * Accept header
This will be parsed and the types found will be ordered by the
relative quality specified for each type.
=back
lib/Catalyst/TraitFor/Request/REST/ForBrowsers.pm view on Meta::CPAN
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
lib/Catalyst/TraitFor/Request/REST/ForBrowsers.pm view on Meta::CPAN
=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 *
t/catalyst-action-serialize-accept.t view on Meta::CPAN
$req->header('Accept', 'text/x-yaml');
my $res = request($req);
SKIP: {
skip "can't test text/x-yaml without YAML support",
3 if (
not $res->is_success and
$res->content =~ m#Content-Type text/x-yaml is not supported#
);
ok( $res->is_success, 'GET the serialized request succeeded' );
is( $res->content, $output_YAML, "Request returned proper data");
is( $res->content_type, 'text/x-yaml', '... with expected content-type')
};
}
SKIP: {
eval 'use JSON 2.12;';
skip "can't test application/json without JSON support", 3 if $@;
my $json = JSON->new;
my $at = Test::Rest->new('content_type' => 'text/doesnt-exist');
my $req = $at->get(url => '/serialize/test');
$req->header('Accept', 'application/json');
my $res = request($req);
ok( $res->is_success, 'GET the serialized request succeeded' );
my $ret = $json->decode($res->content);
is( $ret->{lou}, 'is my cat', "Request returned proper data");
is( $res->content_type, 'application/json', 'Accept header used if content-type mapping not found')
};
# Make sure we don't get a bogus content-type when using the default
# serializer (https://rt.cpan.org/Ticket/Display.html?id=27949)
{
my $req = $t->get(url => '/serialize/test');
$req->remove_header('Content-Type');
$req->header('Accept', '*/*');
my $res = request($req);
ok( $res->is_success, 'GET the serialized request succeeded' );
is( $res->content, $output_YAML, "Request returned proper data");
is( $res->content_type, 'text/x-yaml', '... with expected content-type')
}
# Make sure that when using content_type_stash_key, an invalid value in the stash gets ignored
{
my $req = $t->get(url => '/serialize/test_second?serialize_content_type=nonesuch');
$req->remove_header('Content-Type');
$req->header('Accept', '*/*');
my $res = request($req);
ok( $res->is_success, 'GET the serialized request succeeded' );
is( $res->content, $output_YAML, "Request returned proper data");
is( $res->content_type, 'text/x-yaml', '... with expected content-type')
}
# 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' );
like( $res->content, qr/\A--- ?\nlou: is my cat\n\z/, "Request returned proper data");
}
t/catalyst-action-serialize-query.t view on Meta::CPAN
use FindBin;
use lib ("$FindBin::Bin/lib", "$FindBin::Bin/../lib", "$FindBin::Bin/broken");
use Test::Rest;
# YAML
my $t = Test::Rest->new('content_type' => 'text/x-yaml');
use_ok 'Catalyst::Test', 'Test::Catalyst::Action::REST';
my $req = $t->get(url => '/serialize/test?content-type=text/x-yaml');
$req->remove_header('Content-Type');
my $res = request($req);
ok( $res->is_success, 'GET the serialized request succeeded' );
like( $res->content, qr/\A--- ?\nlou: is my cat\n\z/, "Request returned proper data");
1;
done_testing;
t/catalyst-traitfor-request-rest-forbrowsers.t view on Meta::CPAN
);
}
}
{
my $req = $class->new(
_log => Catalyst::Log->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(
_log => Catalyst::Log->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(
_log => Catalyst::Log->new,
);
$req->{_context} = 'MockContext';
$req->method('GET');
$req->parameters( {} );
t/catalyst-traitfor-request-rest.t view on Meta::CPAN
my $request = $class->new(
_log => Catalyst::Log->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 = $class->new( _log => Catalyst::Log->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 = $class->new( _log => Catalyst::Log->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 = $class->new( _log => Catalyst::Log->new );
$request->{_context} = 'MockContext';
$request->headers( HTTP::Headers->new );
t/catalyst-traitfor-request-rest.t view on Meta::CPAN
);
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 = $class->new( _log => Catalyst::Log->new );
$request->{_context} = 'MockContext';
$request->headers( HTTP::Headers->new );
$request->parameters( {} );
$request->method('GET');
$request->content_type('application/json');
t/catalyst-traitfor-request-rest.t view on Meta::CPAN
);
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 = $class->new( _log => Catalyst::Log->new );
$request->{_context} = 'MockContext';
$request->headers( HTTP::Headers->new );
$request->parameters( {} );
$request->method('GET');
$request->content_type('text/x-json');