From dfaf98a6958e11b3af673a2cc3bae60c21aa9ede Mon Sep 17 00:00:00 2001
From: Joe Woodward <joe@oozou.com>
Date: Mon, 17 Sep 2018 13:56:45 +0700
Subject: [PATCH] Render error when unable to deserialize resource

---
 .../initializer/templates/initializer.rb      | 14 ++++++
 lib/jsonapi/rails/configuration.rb            | 13 +++++-
 .../rails/controller/deserialization.rb       |  1 +
 lib/jsonapi/rails/controller/hooks.rb         |  6 +++
 spec/deserialization_spec.rb                  | 43 +++++++++++++++++++
 .../initializers/new_framework_defaults.rb    |  4 +-
 6 files changed, 79 insertions(+), 2 deletions(-)

diff --git a/lib/generators/jsonapi/initializer/templates/initializer.rb b/lib/generators/jsonapi/initializer/templates/initializer.rb
index 36a9774..d9c335c 100644
--- a/lib/generators/jsonapi/initializer/templates/initializer.rb
+++ b/lib/generators/jsonapi/initializer/templates/initializer.rb
@@ -69,6 +69,20 @@
   # # Set a default pagination scheme.
   # config.jsonapi_pagination = ->(_) { {} }
   #
+  # # Set the default action when the payload cannot be deserialized
+  # config.jsonapi_payload_malformed = -> {
+  #   render jsonapi_errors: {
+  #     title: 'Non-compliant Request Body',
+  #     detail: 'The request was not formatted in compliance with the application/vnd.api+json spec',
+  #     links: {
+  #       about: 'http://jsonapi.org/format/'
+  #     }
+  #   }, status: :bad_request
+  # }
+  #
+  # # Uncomment to take no action when the payload cannot be deserialized
+  # config.jsonapi_payload_malformed = nil
+  #
   # # Set a logger.
   # config.logger = Logger.new(STDOUT)
   #
diff --git a/lib/jsonapi/rails/configuration.rb b/lib/jsonapi/rails/configuration.rb
index 7a7f76f..57f1332 100644
--- a/lib/jsonapi/rails/configuration.rb
+++ b/lib/jsonapi/rails/configuration.rb
@@ -39,12 +39,22 @@ module Configurable
 
       DEFAULT_JSONAPI_PAGINATION = ->(_) { {} }
 
+      DEFAULT_JSONAPI_PAYLOAD_MALFORMED = -> {
+        render jsonapi_errors: {
+          title: 'Non-compliant Request Body',
+          detail: 'The request was not formatted in compliance with the application/vnd.api+json spec',
+          links: {
+            about: 'http://jsonapi.org/format/'
+          }
+        }, status: :bad_request
+      }
+
       DEFAULT_LOGGER = Logger.new(STDERR)
 
       DEFAULT_CONFIG = {
+        jsonapi_cache:   DEFAULT_JSONAPI_CACHE,
         jsonapi_class: DEFAULT_JSONAPI_CLASS,
         jsonapi_errors_class: DEFAULT_JSONAPI_ERRORS_CLASS,
-        jsonapi_cache:   DEFAULT_JSONAPI_CACHE,
         jsonapi_expose:  DEFAULT_JSONAPI_EXPOSE,
         jsonapi_fields:  DEFAULT_JSONAPI_FIELDS,
         jsonapi_include: DEFAULT_JSONAPI_INCLUDE,
@@ -52,6 +62,7 @@ module Configurable
         jsonapi_meta:    DEFAULT_JSONAPI_META,
         jsonapi_object:  DEFAULT_JSONAPI_OBJECT,
         jsonapi_pagination: DEFAULT_JSONAPI_PAGINATION,
+        jsonapi_payload_malformed: DEFAULT_JSONAPI_PAYLOAD_MALFORMED,
         logger: DEFAULT_LOGGER
       }.freeze
 
diff --git a/lib/jsonapi/rails/controller/deserialization.rb b/lib/jsonapi/rails/controller/deserialization.rb
index c89934e..33185c1 100644
--- a/lib/jsonapi/rails/controller/deserialization.rb
+++ b/lib/jsonapi/rails/controller/deserialization.rb
@@ -55,6 +55,7 @@ def deserializable_resource(key, options = {}, &block)
                   "Unable to deserialize #{key} because no JSON API payload was" \
                   " found. (#{controller.controller_name}##{params[:action]})"
                 end
+                controller.jsonapi_payload_malformed
                 next
               end
 
diff --git a/lib/jsonapi/rails/controller/hooks.rb b/lib/jsonapi/rails/controller/hooks.rb
index d18a6c6..49370e3 100644
--- a/lib/jsonapi/rails/controller/hooks.rb
+++ b/lib/jsonapi/rails/controller/hooks.rb
@@ -69,6 +69,12 @@ def jsonapi_meta
         def jsonapi_pagination(resources)
           instance_exec(resources, &JSONAPI::Rails.config[:jsonapi_pagination])
         end
+
+        # Hook for rendering jsonapi_errors when no payload passed
+        def jsonapi_payload_malformed
+          return unless JSONAPI::Rails.config[:jsonapi_payload_malformed]
+          instance_exec(&JSONAPI::Rails.config[:jsonapi_payload_malformed])
+        end
       end
     end
   end
diff --git a/spec/deserialization_spec.rb b/spec/deserialization_spec.rb
index f7a0454..90143d0 100644
--- a/spec/deserialization_spec.rb
+++ b/spec/deserialization_spec.rb
@@ -93,4 +93,47 @@ def create
       expect(controller.jsonapi_pointers).to eq(expected)
     end
   end
+
+  context 'when unable to deserialize resource from params' do
+    controller do
+      deserializable_resource :user
+
+      def create
+        render plain: :ok
+      end
+    end
+
+    context 'with the default config' do
+      it 'makes the deserialized resource available in params' do
+        post :create
+
+        expect(response.body).to eq(
+          {
+            :errors => [
+              {
+                :links => {
+                :about => "http://jsonapi.org/format/"
+              },
+              :title => "Non-compliant Request Body",
+              :detail => "The request was not formatted in compliance with the application/vnd.api+json spec"
+              }
+            ],
+            :jsonapi => {
+              :version=>"1.0"
+            }
+          }.to_json
+        )
+        expect(response).to be_bad_request
+      end
+    end
+
+    context 'when the config[:jsonapi_payload_malformed] == nil' do
+      it 'makes the deserialization mapping available via #jsonapi_pointers' do
+        with_config(jsonapi_payload_malformed: nil) { post :create }
+
+        expect(response.body).to eq('ok')
+        expect(response).to be_success
+      end
+    end
+  end
 end
diff --git a/spec/dummy/config/initializers/new_framework_defaults.rb b/spec/dummy/config/initializers/new_framework_defaults.rb
index 0706caf..b6c8df7 100644
--- a/spec/dummy/config/initializers/new_framework_defaults.rb
+++ b/spec/dummy/config/initializers/new_framework_defaults.rb
@@ -18,7 +18,9 @@
 Rails.application.config.active_record.belongs_to_required_by_default = true
 
 # Do not halt callback chains when a callback returns false. Previous versions had true.
-ActiveSupport.halt_callback_chains_on_return_false = false
+if Rails.version.to_f < 5.2
+  ActiveSupport.halt_callback_chains_on_return_false = false
+end
 
 # Configure SSL options to enable HSTS with subdomains. Previous versions had false.
 Rails.application.config.ssl_options = { hsts: { subdomains: true } }