diff --git a/jsonapi-rails.gemspec b/jsonapi-rails.gemspec index b618573..caacf32 100644 --- a/jsonapi-rails.gemspec +++ b/jsonapi-rails.gemspec @@ -14,7 +14,7 @@ Gem::Specification.new do |spec| spec.files = Dir['README.md', 'lib/**/*'] spec.require_path = 'lib' - spec.add_dependency 'jsonapi-rb', '~> 0.3.0' + spec.add_dependency 'jsonapi-rb', '~> 0.5.0' spec.add_dependency 'jsonapi-parser', '~> 0.1.0' spec.add_development_dependency 'rails', '~> 5.0' diff --git a/lib/generators/jsonapi/initializer/templates/initializer.rb b/lib/generators/jsonapi/initializer/templates/initializer.rb index 017e8ee..63ac518 100644 --- a/lib/generators/jsonapi/initializer/templates/initializer.rb +++ b/lib/generators/jsonapi/initializer/templates/initializer.rb @@ -1,6 +1,31 @@ JSONAPI::Rails.configure do |config| - # config.register_mime_type = true - # config.register_param_parser = true - # config.register_renderers = true - # config.extend_action_controller = true + # # Set a default serializable class mapping. + # config.jsonapi_class = Hash.new { |h, k| + # names = k.to_s.split('::') + # klass = names.pop + # h[k] = [*names, "Serializable#{klass}"].join('::').safe_constantize + # } + # + # # Set a default serializable class mapping for errors. + # config.jsonapi_errors_class = Hash.new { |h, k| + # names = k.to_s.split('::') + # klass = names.pop + # h[k] = [*names, "Serializable#{klass}"].join('::').safe_constantize + # }.tap { |h| + # h[:'ActiveModel::Errors'] = JSONAPI::Rails::SerializableActiveModelErrors + # h[:Hash] = JSONAPI::Rails::SerializableErrorHash + # } + # + # # Set a default JSON API object. + # config.jsonapi_object = { + # version: '1.0' + # } + # + # # Set default exposures. + # config.jsonapi_expose = { + # url_helpers: ::Rails.application.routes.url_helpers + # } + # + # # Set a default pagination scheme. + # config.jsonapi_pagination = ->(_) { nil } end diff --git a/lib/jsonapi/rails/configuration.rb b/lib/jsonapi/rails/configuration.rb index e1e7bd3..97f8ec9 100644 --- a/lib/jsonapi/rails/configuration.rb +++ b/lib/jsonapi/rails/configuration.rb @@ -1,12 +1,37 @@ +require 'jsonapi/rails/serializable_active_model_errors' +require 'jsonapi/rails/serializable_error_hash' + module JSONAPI module Rails class Configuration < ActiveSupport::InheritableOptions; end + DEFAULT_JSONAPI_CLASS = Hash.new do |h, k| + names = k.to_s.split('::') + klass = names.pop + h[k] = [*names, "Serializable#{klass}"].join('::').safe_constantize + end.freeze + + DEFAULT_JSONAPI_ERRORS_CLASS = DEFAULT_JSONAPI_CLASS.dup.merge!( + 'ActiveModel::Errors'.to_sym => + JSONAPI::Rails::SerializableActiveModelErrors, + 'Hash'.to_sym => JSONAPI::Rails::SerializableErrorHash + ).freeze + + DEFAULT_JSONAPI_OBJECT = { + version: '1.0' + }.freeze + + DEFAULT_JSONAPI_EXPOSE = { + url_helpers: ::Rails.application.routes.url_helpers + }.freeze + + DEFAULT_JSONAPI_PAGINATION = ->(_) { nil } DEFAULT_CONFIG = { - register_parameter_parser: true, - register_mime_type: true, - register_renderers: true, - extend_action_controller: true + jsonapi_class: DEFAULT_JSONAPI_CLASS, + jsonapi_errors_class: DEFAULT_JSONAPI_ERRORS_CLASS, + jsonapi_object: DEFAULT_JSONAPI_OBJECT, + jsonapi_expose: DEFAULT_JSONAPI_EXPOSE, + jsonapi_pagination: DEFAULT_JSONAPI_PAGINATION }.freeze def self.configure diff --git a/lib/jsonapi/rails/controller.rb b/lib/jsonapi/rails/controller.rb index 8f75a33..3899235 100644 --- a/lib/jsonapi/rails/controller.rb +++ b/lib/jsonapi/rails/controller.rb @@ -1,9 +1,11 @@ require 'jsonapi/deserializable' require 'jsonapi/parser' +require 'jsonapi/rails/configuration' module JSONAPI module Rails module Deserializable + # @private class Resource < JSONAPI::Deserializable::Resource id type @@ -20,22 +22,56 @@ class Resource < JSONAPI::Deserializable::Resource end end + # ActionController methods and hooks for JSON API deserialization and + # rendering. module Controller extend ActiveSupport::Concern - JSONAPI_POINTERS_KEY = 'jsonapi_deserializable.jsonapi_pointers'.freeze + JSONAPI_POINTERS_KEY = 'jsonapi-rails.jsonapi_pointers'.freeze class_methods do + # Declare a deserializable resource. + # + # @param key [Symbol] The key under which the deserialized hash will be + # available within the `params` hash. + # @param options [Hash] + # @option class [Class] A custom deserializer class. Optional. + # @option only List of actions for which deserialization should happen. + # Optional. + # @option except List of actions for which deserialization should not + # happen. Optional. + # @yieldreturn Optional block for in-line definition of custom + # deserializers. + # + # @example + # class ArticlesController < ActionController::Base + # deserializable_resource :article, only: [:create, :update] + # + # def create + # article = Article.new(params[:article]) + # + # if article.save + # render jsonapi: article + # else + # render jsonapi_errors: article.errors + # end + # end + # + # # ... + # end + # + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize def deserializable_resource(key, options = {}, &block) options = options.dup klass = options.delete(:class) || Class.new(JSONAPI::Rails::Deserializable::Resource, &block) before_action(options) do |controller| + # TODO(lucas): Fail with helpful error message if _jsonapi not + # present. hash = controller.params[:_jsonapi].to_unsafe_hash - ActiveSupport::Notifications.instrument('parse.jsonapi', - payload: hash, - class: klass) do + ActiveSupport::Notifications + .instrument('parse.jsonapi', payload: hash, class: klass) do JSONAPI::Parser::Resource.parse!(hash) resource = klass.new(hash[:data]) controller.request.env[JSONAPI_POINTERS_KEY] = @@ -44,24 +80,46 @@ def deserializable_resource(key, options = {}, &block) end end end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize end + # Hook for serializable class mapping (for resources). + # Overridden by the `class` renderer option. + # @return [Hash{Symbol=>Class}] + def jsonapi_class + JSONAPI::Rails.config[:jsonapi_class] + end + + # Hook for serializable class mapping (for errors). + # Overridden by the `class` renderer option. + # @return [Hash{Symbol=>Class}] + def jsonapi_errors_class + JSONAPI::Rails.config[:jsonapi_errors_class] + end + + # Hook for the jsonapi object. + # Overridden by the `jsonapi_object` renderer option. + # @return [Hash] def jsonapi_object - nil + JSONAPI::Rails.config[:jsonapi_object] end + # Hook for default exposures. + # @return [Hash] def jsonapi_expose - { - url_helpers: ::Rails.application.routes.url_helpers - } + JSONAPI::Rails.config[:jsonapi_expose] end - def jsonapi_pagination(_collection) - nil + # Hook for pagination scheme. + # @return [Hash] + def jsonapi_pagination(resources) + instance_exec(resources, &JSONAPI::Rails.config[:jsonapi_pagination]) end + # JSON pointers for deserialized fields. + # @return [Hash{Symbol=>String}] def jsonapi_pointers - request.env[JSONAPI_POINTERS_KEY] + request.env[JSONAPI_POINTERS_KEY] || {} end end end diff --git a/lib/jsonapi/rails/parser.rb b/lib/jsonapi/rails/parser.rb deleted file mode 100644 index 6774559..0000000 --- a/lib/jsonapi/rails/parser.rb +++ /dev/null @@ -1,12 +0,0 @@ -require 'jsonapi/deserializable' - -module JSONAPI - module Rails - PARSER = lambda do |body| - data = JSON.parse(body) - hash = { _jsonapi: data } - - hash.with_indifferent_access - end - end -end diff --git a/lib/jsonapi/rails/railtie.rb b/lib/jsonapi/rails/railtie.rb index 5aa6a7f..b54b90d 100644 --- a/lib/jsonapi/rails/railtie.rb +++ b/lib/jsonapi/rails/railtie.rb @@ -2,56 +2,66 @@ require 'action_controller' require 'active_support' -require 'jsonapi/rails/configuration' -require 'jsonapi/rails/controller' -require 'jsonapi/rails/parser' require 'jsonapi/rails/renderer' module JSONAPI module Rails + # @private class Railtie < ::Rails::Railtie MEDIA_TYPE = 'application/vnd.api+json'.freeze + PARSER = lambda do |body| + data = JSON.parse(body) + hash = { _jsonapi: data } + + hash.with_indifferent_access + end RENDERERS = { - jsonapi: SuccessRenderer.new, - jsonapi_error: ErrorsRenderer.new + jsonapi: SuccessRenderer.new, + jsonapi_errors: ErrorsRenderer.new }.freeze - initializer 'jsonapi.init', after: :load_config_initializers do - if JSONAPI::Rails.config.register_mime_type - Mime::Type.register MEDIA_TYPE, :jsonapi + initializer 'jsonapi-rails.init' do + register_mime_type + register_parameter_parser + register_renderers + ActiveSupport.on_load(:action_controller) do + require 'jsonapi/rails/controller' + include ::JSONAPI::Rails::Controller end + end - if JSONAPI::Rails.config.register_parameter_parser - if ::Rails::VERSION::MAJOR >= 5 - ::ActionDispatch::Request.parameter_parsers[:jsonapi] = PARSER - else - ::ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = PARSER - end - end + private - if JSONAPI::Rails.config.extend_action_controller - ActiveSupport.on_load(:action_controller) do - include ::JSONAPI::Rails::Controller - end + def register_mime_type + Mime::Type.register(MEDIA_TYPE, :jsonapi) + end + + def register_parameter_parser + if ::Rails::VERSION::MAJOR >= 5 + ActionDispatch::Request.parameter_parsers[:jsonapi] = PARSER + else + ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = PARSER end + end + + # rubocop:disable Metrics/MethodLength + def register_renderers + ActiveSupport.on_load(:action_controller) do + 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] - if JSONAPI::Rails.config.register_renderers - ActiveSupport.on_load(:action_controller) do - 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] - - ActiveSupport::Notifications.instrument('render.jsonapi', - resources: resources, - options: options) do - renderer.render(resources, options, self).to_json - end + ActiveSupport::Notifications.instrument('render.jsonapi', + resources: resources, + options: options) do + renderer.render(resources, options, self).to_json end end end end end + # rubocop:enable Metrics/MethodLength end end end diff --git a/lib/jsonapi/rails/renderer.rb b/lib/jsonapi/rails/renderer.rb index c0583b5..f74005f 100644 --- a/lib/jsonapi/rails/renderer.rb +++ b/lib/jsonapi/rails/renderer.rb @@ -2,40 +2,63 @@ module JSONAPI module Rails + # @private class SuccessRenderer - def initialize(renderer = JSONAPI::Serializable::SuccessRenderer.new) + def initialize(renderer = JSONAPI::Serializable::Renderer.new) @renderer = renderer freeze end def render(resources, options, controller) - options = options.dup - - if (pagination_links = controller.jsonapi_pagination(resources)) - (options[:links] ||= {}).merge!(pagination_links) - end - options[:expose] = - controller.jsonapi_expose.merge!(options[:expose] || {}) - options[:jsonapi] = - options[:jsonapi_object] || controller.jsonapi_object + options = default_options(options, controller, resources) @renderer.render(resources, options) end + + private + + def default_options(options, controller, resources) + options.dup.tap do |opts| + opts[:class] ||= controller.jsonapi_class + if (pagination_links = controller.jsonapi_pagination(resources)) + opts[:links] = (opts[:links] || {}).merge(pagination_links) + end + opts[:expose] = controller.jsonapi_expose.merge(opts[:expose] || {}) + opts[:jsonapi] = opts.delete(:jsonapi_object) || + controller.jsonapi_object + end + end end + # @private class ErrorsRenderer - def initialize(renderer = JSONAPI::Serializable::ErrorsRenderer.new) + def initialize(renderer = JSONAPI::Serializable::Renderer.new) @renderer = renderer freeze end def render(errors, options, controller) - options = options.merge(_jsonapi_pointers: controller.jsonapi_pointers) - # TODO(beauby): SerializableError inference on AR validation errors. + options = default_options(options, controller) - @renderer.render(errors, options) + errors = [errors] unless errors.is_a?(Array) + + @renderer.render_errors(errors, options) + end + + private + + def default_options(options, controller) + options.dup.tap do |opts| + opts[:class] ||= controller.jsonapi_errors_class + opts[:expose] = + controller.jsonapi_expose + .merge(opts[:expose] || {}) + .merge!(_jsonapi_pointers: controller.jsonapi_pointers) + opts[:jsonapi] = opts.delete(:jsonapi_object) || + controller.jsonapi_object + end end end end diff --git a/lib/jsonapi/rails/serializable_active_model_errors.rb b/lib/jsonapi/rails/serializable_active_model_errors.rb new file mode 100644 index 0000000..a97ec71 --- /dev/null +++ b/lib/jsonapi/rails/serializable_active_model_errors.rb @@ -0,0 +1,38 @@ +module JSONAPI + module Rails + # @private + class SerializableActiveModelError < Serializable::Error + title do + "Invalid #{@field}" unless @field.nil? + end + + detail do + @message + end + + source do + pointer @pointer unless @pointer.nil? + end + end + + # @private + class SerializableActiveModelErrors + def initialize(exposures) + @errors = exposures[:object] + @reverse_mapping = exposures[:_jsonapi_pointers] || {} + + freeze + end + + def as_jsonapi + @errors.keys.flat_map do |key| + @errors.full_messages_for(key).map do |message| + SerializableActiveModelError.new(field: key, message: message, + pointer: @reverse_mapping[key]) + .as_jsonapi + end + end + end + end + end +end diff --git a/lib/jsonapi/rails/serializable_error_hash.rb b/lib/jsonapi/rails/serializable_error_hash.rb new file mode 100644 index 0000000..92c34f4 --- /dev/null +++ b/lib/jsonapi/rails/serializable_error_hash.rb @@ -0,0 +1,13 @@ +module JSONAPI + module Rails + # @private + class SerializableErrorHash < JSONAPI::Serializable::Error + def initialize(exposures) + super + exposures[:object].each do |k, v| + instance_variable_set("@_#{k}", v) + end + end + end + end +end diff --git a/spec/action_controller_spec.rb b/spec/action_controller_spec.rb deleted file mode 100644 index 6e1f7bb..0000000 --- a/spec/action_controller_spec.rb +++ /dev/null @@ -1,158 +0,0 @@ -require 'rails_helper' - -describe ActionController::Base, type: :controller do - describe '.deserializable_resource' do - let(:payload) do - { - _jsonapi: { - 'data' => { - 'type' => 'users', - 'attributes' => { 'name' => 'Lucas' } - } - } - } - end - - context 'when using default deserializer' do - controller do - deserializable_resource :user - - def create - render plain: 'ok' - end - end - - it 'makes the deserialized resource available in params' do - post :create, params: payload - - expected = { 'type' => 'users', 'name' => 'Lucas' } - expect(controller.params[:user]).to eq(expected) - end - - it 'makes the deserialization mapping available via #jsonapi_pointers' do - post :create, params: payload - - expected = { name: '/data/attributes/name', - type: '/data/type' } - expect(controller.jsonapi_pointers).to eq(expected) - end - end - - context 'when using a customized deserializer' do - controller do - deserializable_resource :user do - attribute(:name) do |val| - { 'first_name'.to_sym => val } - end - end - - def create - render plain: 'ok' - end - end - - it 'makes the deserialized resource available in params' do - post :create, params: payload - - expected = { 'type' => 'users', 'first_name' => 'Lucas' } - expect(controller.params[:user]).to eq(expected) - end - - it 'makes the deserialization mapping available via #jsonapi_pointers' do - post :create, params: payload - - expected = { first_name: '/data/attributes/name', - type: '/data/type' } - expect(controller.jsonapi_pointers).to eq(expected) - end - end - - context 'when using a customized deserializer with key_format' do - controller do - deserializable_resource :user do - key_format(&:capitalize) - end - - def create - render plain: 'ok' - end - end - - it 'makes the deserialized resource available in params' do - post :create, params: payload - - expected = { 'type' => 'users', 'Name' => 'Lucas' } - expect(controller.params[:user]).to eq(expected) - end - - it 'makes the deserialization mapping available via #jsonapi_pointers' do - post :create, params: payload - - expected = { Name: '/data/attributes/name', - type: '/data/type' } - expect(controller.jsonapi_pointers).to eq(expected) - end - end - end - - describe '#render' do - context 'when calling render jsonapi: user' do - controller do - def index - serializer = Class.new(JSONAPI::Serializable::Resource) do - type :users - attribute :name - end - user = OpenStruct.new(id: 1, name: 'Lucas') - - render jsonapi: user, class: serializer - end - end - - subject { JSON.parse(response.body) } - let(:serialized_user) do - { - 'data' => { - 'id' => '1', - 'type' => 'users', - 'attributes' => { 'name' => 'Lucas' } - } - } - end - - it 'renders a JSON API success document' do - get :index - - expect(response.content_type).to eq('application/vnd.api+json') - is_expected.to eq(serialized_user) - end - end - - context 'when specifying a default jsonapi object' do - controller do - def index - render jsonapi: nil - end - - def jsonapi_object - { version: '1.0' } - end - end - - subject { JSON.parse(response.body) } - let(:document) do - { - 'data' => nil, - 'jsonapi' => { 'version' => '1.0' } - } - end - - it 'renders a JSON API success document' do - get :index - - expect(response.content_type).to eq('application/vnd.api+json') - is_expected.to eq(document) - end - end - end -end diff --git a/spec/config_spec.rb b/spec/config_spec.rb index 67ec9c3..1145ac1 100644 --- a/spec/config_spec.rb +++ b/spec/config_spec.rb @@ -1,39 +1,5 @@ require 'rails_helper' describe JSONAPI::Rails.config do - context 'when the default configuration is used' do - it 'should register the jsonapi parameter parser' do - expect(JSONAPI::Rails.config.register_parameter_parser).to be true - end - - it 'should register the jsonapi mime type' do - expect(JSONAPI::Rails.config.register_mime_type).to be true - end - - it 'should register the jsonapi renderers' do - expect(JSONAPI::Rails.config.register_renderers).to be true - end - end - - context 'when a custom configuration is used' do - before do - JSONAPI::Rails.configure do |config| - config.register_parameter_parser = false - config.register_mime_type = false - config.register_renderers = false - end - end - - it 'should not register the jsonapi parameter parser' do - expect(JSONAPI::Rails.config.register_parameter_parser).to be false - end - - it 'should not register the jsonapi mime type' do - expect(JSONAPI::Rails.config.register_mime_type).to be false - end - - it 'should not register the jsonapi renderers' do - expect(JSONAPI::Rails.config.register_renderers).to be false - end - end + # TODO(lucas) end diff --git a/spec/deserialization_spec.rb b/spec/deserialization_spec.rb new file mode 100644 index 0000000..f7a0454 --- /dev/null +++ b/spec/deserialization_spec.rb @@ -0,0 +1,96 @@ +require 'rails_helper' + +describe ActionController::Base, '.deserializable_resource', + type: :controller do + let(:payload) do + { + _jsonapi: { + 'data' => { + 'type' => 'users', + 'attributes' => { 'name' => 'Lucas' } + } + } + } + end + + context 'when using default deserializer' do + controller do + deserializable_resource :user + + def create + render plain: 'ok' + end + end + + it 'makes the deserialized resource available in params' do + post :create, params: payload + + expected = { 'type' => 'users', 'name' => 'Lucas' } + expect(controller.params[:user]).to eq(expected) + end + + it 'makes the deserialization mapping available via #jsonapi_pointers' do + post :create, params: payload + + expected = { name: '/data/attributes/name', + type: '/data/type' } + expect(controller.jsonapi_pointers).to eq(expected) + end + end + + context 'when using a customized deserializer' do + controller do + deserializable_resource :user do + attribute(:name) do |val| + { 'first_name'.to_sym => val } + end + end + + def create + render plain: 'ok' + end + end + + it 'makes the deserialized resource available in params' do + post :create, params: payload + + expected = { 'type' => 'users', 'first_name' => 'Lucas' } + expect(controller.params[:user]).to eq(expected) + end + + it 'makes the deserialization mapping available via #jsonapi_pointers' do + post :create, params: payload + + expected = { first_name: '/data/attributes/name', + type: '/data/type' } + expect(controller.jsonapi_pointers).to eq(expected) + end + end + + context 'when using a customized deserializer with key_format' do + controller do + deserializable_resource :user do + key_format(&:capitalize) + end + + def create + render plain: 'ok' + end + end + + it 'makes the deserialized resource available in params' do + post :create, params: payload + + expected = { 'type' => 'users', 'Name' => 'Lucas' } + expect(controller.params[:user]).to eq(expected) + end + + it 'makes the deserialization mapping available via #jsonapi_pointers' do + post :create, params: payload + + expected = { Name: '/data/attributes/name', + type: '/data/type' } + expect(controller.jsonapi_pointers).to eq(expected) + end + end +end diff --git a/spec/render_jsonapi_errors_spec.rb b/spec/render_jsonapi_errors_spec.rb new file mode 100644 index 0000000..6250d36 --- /dev/null +++ b/spec/render_jsonapi_errors_spec.rb @@ -0,0 +1,84 @@ +require 'rails_helper' + +describe ActionController::Base, '#render', type: :controller do + let(:serialized_errors) do + { + 'errors' => [ + { + 'detail' => 'Name can\'t be blank', + 'title' => 'Invalid name', + 'source' => { 'pointer' => '/data/attributes/name' } + }, + { + 'detail' => 'Email must be a valid email', + 'title' => 'Invalid email', + 'source' => { 'pointer' => '/data/attributes/email' } + } + ], + 'jsonapi' => { 'version' => '1.0' } + } + end + + context 'when rendering ActiveModel::Errors' do + class User < ActiveRecord::Base + validates :name, presence: true + validates :email, format: { with: /@/, message: 'must be a valid email' } + end + + controller do + def create + user = User.new(email: 'lucas') + + unless user.valid? + render jsonapi_errors: user.errors + end + end + + def jsonapi_pointers + { + name: '/data/attributes/name', + email: '/data/attributes/email' + } + end + end + + subject { JSON.parse(response.body) } + + it 'renders a JSON API error document' do + post :create + + expect(response.content_type).to eq('application/vnd.api+json') + is_expected.to eq(serialized_errors) + end + end + + context 'when rendering error hashes' do + controller do + def create + errors = [ + { + detail: 'Name can\'t be blank', + title: 'Invalid name', + source: { pointer: '/data/attributes/name' } + }, + { + detail: 'Email must be a valid email', + title: 'Invalid email', + source: { pointer: '/data/attributes/email' } + } + ] + + render jsonapi_errors: errors + end + end + + subject { JSON.parse(response.body) } + + it 'renders a JSON API error document' do + post :create + + expect(response.content_type).to eq('application/vnd.api+json') + is_expected.to eq(serialized_errors) + end + end +end diff --git a/spec/render_jsonapi_spec.rb b/spec/render_jsonapi_spec.rb new file mode 100644 index 0000000..6cd373f --- /dev/null +++ b/spec/render_jsonapi_spec.rb @@ -0,0 +1,63 @@ +require 'rails_helper' + +describe ActionController::Base, '#render', type: :controller do + context 'when calling render jsonapi: user' do + controller do + def index + serializer = Class.new(JSONAPI::Serializable::Resource) do + type :users + attribute :name + end + user = OpenStruct.new(id: 1, name: 'Lucas') + + render jsonapi: user, class: { OpenStruct: serializer } + end + end + + subject { JSON.parse(response.body) } + let(:serialized_user) do + { + 'data' => { + 'id' => '1', + 'type' => 'users', + 'attributes' => { 'name' => 'Lucas' } + }, + 'jsonapi' => { 'version' => '1.0' } + } + end + + it 'renders a JSON API success document' do + get :index + + expect(response.content_type).to eq('application/vnd.api+json') + is_expected.to eq(serialized_user) + end + end + + context 'when specifying a custom jsonapi object at controller level' do + controller do + def index + render jsonapi: nil + end + + def jsonapi_object + { version: '2.0' } + end + end + + subject { JSON.parse(response.body) } + let(:document) do + { + 'data' => nil, + 'jsonapi' => { 'version' => '2.0' } + } + end + + it 'renders a JSON API success document' do + get :index + + expect(response.content_type).to eq('application/vnd.api+json') + is_expected.to eq(document) + end + end +end