diff --git a/Gemfile.lock b/Gemfile.lock index 05dac4d..e82fb40 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,7 +3,7 @@ PATH specs: grape-jsonapi (0.0.2) grape (>= 1.4.0) - jsonapi-resources (~> 0.9.11) + jsonapi-resources (~> 0.10.7) GEM remote: https://rubygems.org/ @@ -42,30 +42,26 @@ GEM crass (1.0.6) diff-lcs (1.4.4) docile (1.3.2) - dry-configurable (0.11.6) + dry-configurable (0.14.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.4, >= 0.4.7) - dry-equalizer (~> 0.2) - dry-container (0.7.2) + dry-core (~> 0.6) + dry-container (0.9.0) concurrent-ruby (~> 1.0) - dry-configurable (~> 0.1, >= 0.1.3) - dry-core (0.4.9) + dry-configurable (~> 0.13, >= 0.13.0) + dry-core (0.7.1) concurrent-ruby (~> 1.0) - dry-equalizer (0.3.0) - dry-inflector (0.2.0) - dry-logic (1.0.8) + dry-inflector (0.2.1) + dry-logic (1.2.0) concurrent-ruby (~> 1.0) - dry-core (~> 0.2) - dry-equalizer (~> 0.2) - dry-types (1.4.0) + dry-core (~> 0.5, >= 0.5) + dry-types (1.5.1) concurrent-ruby (~> 1.0) dry-container (~> 0.3) - dry-core (~> 0.4, >= 0.4.4) - dry-equalizer (~> 0.3) + dry-core (~> 0.5, >= 0.5) dry-inflector (~> 0.1, >= 0.1.2) dry-logic (~> 1.0, >= 1.0.2) erubi (1.9.0) - grape (1.5.0) + grape (1.6.2) activesupport builder dry-types (>= 1.1) @@ -79,7 +75,7 @@ GEM concurrent-ruby (~> 1.0) json-schema (2.8.1) addressable (>= 2.4) - jsonapi-resources (0.9.11) + jsonapi-resources (0.10.7) activerecord (>= 4.1) concurrent-ruby railties (>= 4.1) @@ -161,7 +157,7 @@ GEM rubocop-ast (0.8.0) parser (>= 2.7.1.5) ruby-progressbar (1.10.1) - ruby2_keywords (0.0.2) + ruby2_keywords (0.0.5) simplecov (0.19.0) docile (~> 1.1) simplecov-html (~> 0.11) diff --git a/grape-jsonapi.gemspec b/grape-jsonapi.gemspec index 87b649c..1a08980 100644 --- a/grape-jsonapi.gemspec +++ b/grape-jsonapi.gemspec @@ -26,5 +26,5 @@ Gem::Specification.new do |spec| end spec.add_runtime_dependency 'grape', '>= 1.4.0' - spec.add_runtime_dependency 'jsonapi-resources', '~> 0.9.11' + spec.add_runtime_dependency 'jsonapi-resources', '~> 0.10.7' end diff --git a/lib/grape/jsonapi/api.rb b/lib/grape/jsonapi/api.rb index 1059f15..077a477 100644 --- a/lib/grape/jsonapi/api.rb +++ b/lib/grape/jsonapi/api.rb @@ -25,7 +25,7 @@ def jsonapi_resource declare_base_route # Before generating links for resources while building a response, the jsonapi-resources - # gem verifies whether there are routes to them. If no, a warning intead of a link + # gem verifies whether there are routes to them. If no, a warning instead of a link # will be outputed. This gem doesn't use route helpers of the jsonapi-resources gem, # but it adds Grape routes, so a `_routed` flag must be added for every resource # which routed by Grape. diff --git a/lib/grape/jsonapi/helpers.rb b/lib/grape/jsonapi/helpers.rb index 21ec436..0117b1f 100644 --- a/lib/grape/jsonapi/helpers.rb +++ b/lib/grape/jsonapi/helpers.rb @@ -6,10 +6,13 @@ module Grape module JSONAPI # A collection of helper methods used internally by JSON API endpoints. + # Mainly methods were copied from https://bit.ly/3ygyl5y then adjusted. module Helpers include Rendering include Resources + attr_reader :jsonapi_request + def controller_class env['api.endpoint'].options[:for] end @@ -26,38 +29,49 @@ def json_request_for(action, options) action_parameters, # define a `context` method to pass extra data to resources context: respond_to?(:context) ? send(:context) : {}, + key_formatter: key_formatter, server_error_callbacks: [] ) end def process_request(action, options = {}) - @json_request = json_request_for(action.to_sym, options) + begin + @jsonapi_request = json_request_for(action, options) - if @json_request.errors.empty? - results = operation_dispatcher.process(@json_request.operations) - render_results results - else - render_errors(@json_request.errors) + setup_response_document + execute_request + rescue StandardError => e + handle_exceptions(e) end - rescue ::JSONAPI::Exceptions::Error => e - render_errors(e.errors) - end - def operation_dispatcher - ::JSONAPI::OperationDispatcher.new( - transaction: transaction_block, - rollback: rollback_action, - server_error_callbacks: [] - ) + render_results end - def transaction_block - ->(&block) { ActiveRecord::Base.transaction { block.yield } } - end + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + def execute_request + process_operations(jsonapi_request.transactional?) do + jsonapi_request.each(response_document) do |op| + op.options[:serializer] = resource_serializer_klass.new( + op.resource_klass, + include_directives: op.options[:include_directives], + fields: op.options[:fields], + base_url: base_url, + key_formatter: key_formatter, + route_formatter: route_formatter, + serialization_options: {}, + controller: self + ) + op.options[:cache_serializer_output] = !::JSONAPI.configuration.resource_cache.nil? - def rollback_action - -> { fail ActiveRecord::Rollback } + process_operation(op) + end + + fail ActiveRecord::Rollback if response_document.has_errors? + end end + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/AbcSize def forbidden_operation render_errors [::JSONAPI::Error.new( @@ -67,6 +81,54 @@ def forbidden_operation detail: I18n.t('jsonapi.errors.forbidden_operation.detail') )] end + + def process_operations(transactional, &block) + if transactional + ActiveRecord::Base.transaction(&block) + else + begin + block.call + rescue ActiveRecord::Rollback + # Can't rollback without transaction, so just ignore it + end + end + end + + def process_operation(operation) + result = operation.process + response_document.add_result(result, operation) + end + + private + + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/MethodLength + def handle_exceptions(exeption) + case exeption + when ::JSONAPI::Exceptions::Error + errors = exeption.errors + when ActionController::ParameterMissing + errors = ::JSONAPI::Exceptions::ParameterMissing.new(exeption.param).errors + else + fail exeption if ::JSONAPI.configuration.exception_class_whitelisted?(exeption) + + # Store exception for other middlewares + request.env['action_dispatch.exception'] ||= exeption + + internal_server_error = ::JSONAPI::Exceptions::InternalServerError.new(exeption) + + Rails.logger.error do + "Internal Server Error: #{exeption.message} #{exeption.backtrace.join("\n")}" + end + + errors = internal_server_error.errors + end + + response_document.add_result(::JSONAPI::ErrorsOperationResult.new(errors[0].status, errors), + nil) + end + # rubocop:enable Metrics/MethodLength + # rubocop:enable Metrics/AbcSize end end end diff --git a/lib/grape/jsonapi/relationships.rb b/lib/grape/jsonapi/relationships.rb index 0297e36..686c35a 100644 --- a/lib/grape/jsonapi/relationships.rb +++ b/lib/grape/jsonapi/relationships.rb @@ -37,9 +37,9 @@ def declare_related_resource(name, relationship, _options) define_related_resource_helpers(relationship) if to_many?(relationship) - get { process_request(:get_related_resources, related_params) } + get { process_request(:index_related_resources, related_params) } else - get { process_request(:get_related_resource, related_params) } + get { process_request(:show_related_resource, related_params) } end end end diff --git a/lib/grape/jsonapi/rendering.rb b/lib/grape/jsonapi/rendering.rb index 84c8526..9de48cf 100644 --- a/lib/grape/jsonapi/rendering.rb +++ b/lib/grape/jsonapi/rendering.rb @@ -6,39 +6,42 @@ module JSONAPI module Rendering include Resources + attr_reader :response_document + def render_errors(errors) - operation_results = ::JSONAPI::OperationResults.new + setup_response_document + result = ::JSONAPI::ErrorsOperationResult.new( errors.first.status, errors ) - operation_results.add_result(result) + response_document.add_result(result, nil) - render_results operation_results + render_results end - def render_results(operation_results) - response_doc = create_response_document(operation_results) - - response_status(response_doc) - response_doc.status == :no_content ? '' : response_doc.contents + def render_results + response_status + response_document.status == 204 ? '' : response_document.contents end private - def create_response_document(operation_results) + def create_response_document ::JSONAPI::ResponseDocument.new( - operation_results, - operation_results.has_errors? ? nil : resource_serializer, key_formatter: key_formatter, base_meta: base_meta, - base_links: base_links, - request: @json_request + base_links: base_response_links, + request: respond_to?(:request) ? request : nil ) end - def response_status(response_doc) - status_code = response_doc.status + def setup_response_document + @response_document = create_response_document + end + + def response_status + status_code = response_document.status status_code = status_code.to_i if status_code.is_a? String status status_code end @@ -47,6 +50,10 @@ def base_links {} end + def base_response_links + {} + end + def base_meta return {} unless defined? @json_request diff --git a/lib/grape/jsonapi/resources.rb b/lib/grape/jsonapi/resources.rb index 9d1203b..d1c60fb 100644 --- a/lib/grape/jsonapi/resources.rb +++ b/lib/grape/jsonapi/resources.rb @@ -10,7 +10,7 @@ def controller_name end def resource_class - ::JSONAPI::Resource.resource_for controller_name + ::JSONAPI::Resource.resource_klass_for controller_name end def resource_name @@ -26,7 +26,7 @@ def resource_module_name end def related_resource_for(relationship) - ::JSONAPI::Resource.resource_for( + ::JSONAPI::Resource.resource_klass_for( "#{resource_module_name}/#{relationship.class_name}".underscore ) end diff --git a/spec/fixtures/active_record.rb b/spec/fixtures/active_record.rb index 04fc93a..5c6c46d 100644 --- a/spec/fixtures/active_record.rb +++ b/spec/fixtures/active_record.rb @@ -793,7 +793,7 @@ def title=(title) return values }, apply: lambda { |records, value, _options| - records.where('id IN (?)', value) + records.where('posts.id IN (?)', value) } filter :search, @@ -1148,6 +1148,7 @@ class BoatResource < BoatResource; end module Api module V2 + class AuthorResource < PreferencesResource; end class PreferencesResource < PreferencesResource; end class PersonResource < PersonResource; end class PostResource < PostResource; end @@ -1257,6 +1258,7 @@ class PreferencesResource < PreferencesResource; end module Api module V4 + class AuthorResource < PreferencesResource; end class PostResource < PostResource; end class PersonResource < PersonResource; end class ExpenseEntryResource < ExpenseEntryResource; end