From dee01d37c57f2ba6398a42783d775a8e20b291f7 Mon Sep 17 00:00:00 2001 From: Lucas Date: Fri, 15 Sep 2017 13:34:56 +0200 Subject: [PATCH] Add content negotiation middleware + fix response content type. --- lib/jsonapi/rails/filter_media_type.rb | 39 +++++++++++++++ lib/jsonapi/rails/railtie.rb | 7 ++- spec/content_negotiation_spec.rb | 17 +++++++ spec/filter_media_type_spec.rb | 69 ++++++++++++++++++++++++++ spec/railtie_spec.rb | 6 +++ 5 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 lib/jsonapi/rails/filter_media_type.rb create mode 100644 spec/content_negotiation_spec.rb create mode 100644 spec/filter_media_type_spec.rb diff --git a/lib/jsonapi/rails/filter_media_type.rb b/lib/jsonapi/rails/filter_media_type.rb new file mode 100644 index 0000000..3977e1d --- /dev/null +++ b/lib/jsonapi/rails/filter_media_type.rb @@ -0,0 +1,39 @@ +require 'rack/media_type' + +module JSONAPI + module Rails + class FilterMediaType + JSONAPI_MEDIA_TYPE = 'application/vnd.api+json'.freeze + + def initialize(app) + @app = app + end + + def call(env) + return [415, {}, []] unless valid_content_type?(env['CONTENT_TYPE']) + return [406, {}, []] unless valid_accept?(env['HTTP_ACCEPT']) + + @app.call(env) + end + + private + + def valid_content_type?(content_type) + Rack::MediaType.type(content_type) != JSONAPI_MEDIA_TYPE || + Rack::MediaType.params(content_type) == {} + end + + def valid_accept?(accept) + return true if accept.nil? + + jsonapi_media_types = + accept.split(',') + .map(&:strip) + .select { |m| Rack::MediaType.type(m) == JSONAPI_MEDIA_TYPE } + + jsonapi_media_types.empty? || + jsonapi_media_types.any? { |m| Rack::MediaType.params(m) == {} } + end + end + end +end diff --git a/lib/jsonapi/rails/railtie.rb b/lib/jsonapi/rails/railtie.rb index 9d083d8..b91b682 100644 --- a/lib/jsonapi/rails/railtie.rb +++ b/lib/jsonapi/rails/railtie.rb @@ -1,5 +1,6 @@ require 'rails/railtie' +require 'jsonapi/rails/filter_media_type' require 'jsonapi/rails/log_subscriber' require 'jsonapi/rails/renderer' @@ -19,7 +20,7 @@ class Railtie < ::Rails::Railtie jsonapi_errors: ErrorsRenderer.new }.freeze - initializer 'jsonapi-rails.init' do + initializer 'jsonapi-rails.init' do |app| register_mime_type register_parameter_parser register_renderers @@ -27,6 +28,8 @@ class Railtie < ::Rails::Railtie require 'jsonapi/rails/controller' include ::JSONAPI::Rails::Controller end + + app.middleware.use FilterMediaType end private @@ -49,7 +52,7 @@ def register_renderers RENDERERS.each do |name, renderer| ::ActionController::Renderers.add(name) do |resources, options| # Renderer proc is evaluated in the controller context. - self.content_type ||= Mime[:jsonapi] + headers['Content-Type'] = Mime[:jsonapi].to_s ActiveSupport::Notifications.instrument('render.jsonapi-rails', resources: resources, diff --git a/spec/content_negotiation_spec.rb b/spec/content_negotiation_spec.rb new file mode 100644 index 0000000..498e851 --- /dev/null +++ b/spec/content_negotiation_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe ActionController::Base, type: :controller do + controller do + def index + render jsonapi: nil + end + end + + context 'when sending data' do + it 'responds with application/vnd.api+json' do + get :index + + expect(response.headers['Content-Type']).to eq('application/vnd.api+json') + end + end +end diff --git a/spec/filter_media_type_spec.rb b/spec/filter_media_type_spec.rb new file mode 100644 index 0000000..e50774a --- /dev/null +++ b/spec/filter_media_type_spec.rb @@ -0,0 +1,69 @@ +require 'rails_helper' + +describe JSONAPI::Rails::FilterMediaType do + let(:app) { ->(_) { [200, {}, ['OK']] } } + + context 'when not receiving JSON API Content-Type' do + it 'passes through' do + env = { 'CONTENT_TYPE' => 'application/json' } + + expect(described_class.new(app).call(env)[0]).to eq(200) + end + end + + context 'when receiving JSON API Content-Type without media parameters' do + it 'passes through' do + env = { 'CONTENT_TYPE' => 'application/vnd.api+json' } + + expect(described_class.new(app).call(env)[0]).to eq(200) + end + end + + context 'when receiving Content-Type with media parameters' do + it 'fails with 415 Unsupported Media Type' do + env = { 'CONTENT_TYPE' => 'application/vnd.api+json; charset=utf-8' } + + expect(described_class.new(app).call(env)[0]).to eq(415) + end + end + + context 'when not receiving JSON API in Accept' do + it 'passes through' do + env = { 'HTTP_ACCEPT' => 'application/json' } + + expect(described_class.new(app).call(env)[0]).to eq(200) + end + end + + context 'when receiving JSON API in Accept without media parameters' do + it 'passes through' do + env = { 'HTTP_ACCEPT' => 'application/vnd.api+json' } + + expect(described_class.new(app).call(env)[0]).to eq(200) + end + end + + context 'when receiving JSON API in Accept without media parameters among others' do + it 'passes through' do + env = { 'HTTP_ACCEPT' => 'application/json, application/vnd.api+json' } + + expect(described_class.new(app).call(env)[0]).to eq(200) + end + end + + context 'when receiving JSON API in Accept with media parameters' do + it 'fails with 406 Not Acceptable' do + env = { 'HTTP_ACCEPT' => 'application/vnd.api+json; charset=utf-8' } + + expect(described_class.new(app).call(env)[0]).to eq(406) + end + end + + context 'when receiving JSON API in Accept with media parameters among others' do + it 'fails with 406 Not Acceptable' do + env = { 'HTTP_ACCEPT' => 'application/json, application/vnd.api+json; charset=utf-8' } + + expect(described_class.new(app).call(env)[0]).to eq(406) + end + end +end diff --git a/spec/railtie_spec.rb b/spec/railtie_spec.rb index 1b1879d..5a98379 100644 --- a/spec/railtie_spec.rb +++ b/spec/railtie_spec.rb @@ -8,4 +8,10 @@ it 'registers the params parser for the JSON API MIME type' do expect(::ActionDispatch::Request.parameter_parsers[:jsonapi]).not_to be_nil end + + it 'registers the FilterMediaType middleware' do + expect( + Rails.application.middleware.include?(JSONAPI::Rails::FilterMediaType) + ).to be true + end end