From: John Napiorkowski Date: Tue, 5 Dec 2017 15:03:30 +0000 (-0500) Subject: Merge branch 'master' of https://github.com/runarbu/Catalyst-Action-REST into runarbu... X-Git-Tag: v1.21~1^2 X-Git-Url: http://git.shadowcat.co.uk/gitweb/gitweb.cgi?a=commitdiff_plain;h=d971b052b6b49206b9b9fb3a6ee5f3ed37d17489;hp=43e4baa3655de11012897d060a6169dc81b0b4ec;p=catagits%2FCatalyst-Action-REST.git Merge branch 'master' of https://github.com/runarbu/Catalyst-Action-REST into runarbu-master --- diff --git a/lib/Catalyst/Action/Deserialize.pm b/lib/Catalyst/Action/Deserialize.pm index e6a0887..5be60f3 100644 --- a/lib/Catalyst/Action/Deserialize.pm +++ b/lib/Catalyst/Action/Deserialize.pm @@ -117,6 +117,36 @@ Will work just fine. When you use this module, the request class will be changed to L. +=head1 RFC 7231 Compliance Mode + +To maintain backwards compatibility with the module's original functionality, +where it was assumed the deserialize and serialize content types are the same, +an optional compliance mode can be enabled to break this assumption. + + __PACKAGE__->config( + 'compliance_mode' => 1, + 'default' => 'text/x-yaml', + 'stash_key' => 'rest', + 'map' => { + 'text/x-yaml' => 'YAML', + 'text/x-data-dumper' => [ 'Data::Serializer', 'Data::Dumper' ], + }, + 'deserialize_default => 'application/json', + 'deserialize_map' => { + 'application/json' => 'JSON', + }, + ); + +Three extra keys are added to the controller configuration. compliance_mode, a +boolean to enable the mode. And a parallel set of content type mappings +'deserialize_default' and 'deserialize_map' to mirror the default/map +configuration keys. + +The module will use the default/map keys when negotiating the serializing +content type specified by the client in the Accept header. And will use the +deserialize_default/deserialize_map in conjunction with the Content-Type +header where the client is giving the content type being sent in the request. + =head1 CUSTOM ERRORS For building custom error responses when de-serialization fails, you can create diff --git a/lib/Catalyst/Action/SerializeBase.pm b/lib/Catalyst/Action/SerializeBase.pm index b901f61..ba0cdf4 100644 --- a/lib/Catalyst/Action/SerializeBase.pm +++ b/lib/Catalyst/Action/SerializeBase.pm @@ -39,6 +39,8 @@ sub _load_content_plugins { my $sclass = $search_path . "::"; my $sarg; my $map; + my $compliance_mode; + my $default; my $config; @@ -53,6 +55,28 @@ sub _load_content_plugins { $config = $controller; } $map = $config->{'map'}; + $default = $config->{'default'} if $config->{'default'}; + + # If we're in RFC 7231 compliance mode we need to determine if we're + # serializing or deserializing, then set the request object to + # look at the appropriate set of supported content types. + $compliance_mode = $config->{'compliance_mode'}; + if($compliance_mode) { + my $serialize_mode = (split '::', $search_path)[-1]; + if($serialize_mode eq 'Deserialize') { + # Tell the request object to only look at the Content-Type header + $c->request->set_content_type_only(); + + # If we're in compliance mode and doing deserializing we want + # to use the allowed content types for deserializing, not the + # serializer map + $map = $config->{'deserialize_map'}; + $default = $config->{'deserialize_default'} if $config->{'deserialize_default'}; + } elsif($serialize_mode eq 'Serialize') { + # Tell the request object to only look at the Accept header + $c->request->set_accept_only(); + } + } # pick preferred content type my @accepted_types; # priority order, best first @@ -68,7 +92,7 @@ sub _load_content_plugins { # then content types requested by caller push @accepted_types, @{ $c->request->accepted_content_types }; # then the default - push @accepted_types, $config->{'default'} if $config->{'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; diff --git a/lib/Catalyst/Controller/REST.pm b/lib/Catalyst/Controller/REST.pm index 12568f7..b9c10d0 100644 --- a/lib/Catalyst/Controller/REST.pm +++ b/lib/Catalyst/Controller/REST.pm @@ -304,6 +304,7 @@ __PACKAGE__->config( 'application/json' => 'JSON', 'text/x-json' => 'JSON', }, + 'compliance_mode' => 0, ); sub begin : ActionClass('Deserialize') { } diff --git a/lib/Catalyst/TraitFor/Request/REST.pm b/lib/Catalyst/TraitFor/Request/REST.pm index 2287158..91d5eb5 100644 --- a/lib/Catalyst/TraitFor/Request/REST.pm +++ b/lib/Catalyst/TraitFor/Request/REST.pm @@ -11,6 +11,7 @@ has accepted_content_types => ( isa => 'ArrayRef', lazy => 1, builder => '_build_accepted_content_types', + clearer => 'clear_accepted_cache', init_arg => undef, ); @@ -22,24 +23,107 @@ has preferred_content_type => ( init_arg => undef, ); +# +# By default the module looks at both Content-Type and +# Accept and uses the selected content type for both +# deserializing received data and serializing the response. +# However according to RFC 7231, Content-Type should be +# used to specify the payload type of the data sent by +# the requester and Accept should be used to negotiate +# the content type the requester would like back from +# the server. Compliance mode adds support so the method +# described in the RFC is more closely model. +# +# Using a bitmask to represent the the two content type +# header schemes. +# 0x1 for Accept +# 0x2 for Content-Type + +has 'compliance_mode' => ( + is => 'ro', + isa => 'Int', + lazy => 1, + writer => '_set_compliance_mode', + default => 0x3, +); + +# Set request object to only use the Accept header when building +# accepted_content_types +sub set_accept_only { + my $self = shift; + + # Clear the accepted_content_types cache if we've changed + # allowed headers + $self->clear_accepted_cache(); + $self->_set_compliance_mode(0x1); +} + +# Set request object to only use the Content-Type header when building +# accepted_content_types +sub set_content_type_only { + my $self = shift; + + $self->clear_accepted_cache(); + $self->_set_compliance_mode(0x2); +} + +# Clear serialize/deserialize compliance mode, allow all headers +# in both situations +sub clear_compliance_mode { + my $self = shift; + + $self->clear_accepted_cache(); + $self->_set_compliance_mode(0x3); +} + +# Return true if bit set to examine Accept header +sub accept_allowed { + my $self = shift; + + return $self->compliance_mode & 0x1; +} + +# Return true if bit set to examine Content-Type header +sub content_type_allowed { + my $self = shift; + + return $self->compliance_mode & 0x2; +} + +# Private writer to set if we're looking at Accept or Content-Type headers +sub _set_compliance_mode { + my $self = shift; + my $mode_bits = shift; + + $self->compliance_mode($mode_bits); +} + sub _build_accepted_content_types { 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; + if $self->content_type && $self->content_type_allowed(); - if ($self->method eq "GET" && $self->param('content-type')) { + # 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') ) { + if ( $self->header('Accept') && $self->accept_allowed() ) { $self->accept_only(1) unless keys %types; my $accept_header = $self->header('Accept'); @@ -96,7 +180,7 @@ Catalyst::TraitFor::Request::REST - A role to apply to Catalyst::Request giving This is a L applied to L that adds a few 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. +the client and the content type sent in the request. =head1 METHODS