Added an optional mode for RFC 7231 compliance. The Content-Type header is used to...
Matthew Laird [Wed, 22 Nov 2017 11:43:19 +0000 (11:43 +0000)]
This adds a separation between the Serializer and Deserializer when desired. As well, a separate set of mappings for the deserializer content types is allowed. Probably not the most eligant solution but hopefully doesn't break backwards compatibility for those wanting or expecting the current behaviour.

If you have ideas on a better way to do this, I'm open to suggestions on improvements. Thanks.

lib/Catalyst/Action/Deserialize.pm
lib/Catalyst/Action/SerializeBase.pm
lib/Catalyst/Controller/REST.pm
lib/Catalyst/TraitFor/Request/REST.pm

index e6a0887..5be60f3 100644 (file)
@@ -117,6 +117,36 @@ Will work just fine.
 When you use this module, the request class will be changed to
 L<Catalyst::Request::REST>.
 
+=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
index b901f61..ba0cdf4 100644 (file)
@@ -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;
 
index 428d749..3c9ec67 100644 (file)
@@ -293,6 +293,7 @@ __PACKAGE__->config(
         'application/json'   => 'JSON',
         'text/x-json'        => 'JSON',
     },
+    'compliance_mode' => 0,
 );
 
 sub begin : ActionClass('Deserialize') { }
index 2287158..91d5eb5 100644 (file)
@@ -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<Moose::Role> applied to L<Catalyst::Request> 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