diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 3a4673e..5a66a9d 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -1,10 +1,13 @@ require 'jsonapi/deserializable' require 'jsonapi/serializable' -require 'jsonapi/rails/configuration' require 'jsonapi/rails/railtie' module JSONAPI module Rails + require 'jsonapi/rails/configuration' + require 'jsonapi/rails/logging' + extend Configurable + extend Logging end end diff --git a/lib/jsonapi/rails/controller.rb b/lib/jsonapi/rails/controller.rb index e425ebc..a2cc1bb 100644 --- a/lib/jsonapi/rails/controller.rb +++ b/lib/jsonapi/rails/controller.rb @@ -1,163 +1,13 @@ -require 'jsonapi/deserializable' -require 'jsonapi/parser' -require 'jsonapi/rails/configuration' +require 'jsonapi/rails/controller/deserialization' +require 'jsonapi/rails/controller/hooks' module JSONAPI module Rails - module Deserializable - # @private - class Resource < JSONAPI::Deserializable::Resource - id - type - attributes - has_one do |_rel, id, type, key| - type = type.to_s.singularize.camelize - { "#{key}_id".to_sym => id, "#{key}_type".to_sym => type } - end - has_many do |_rel, ids, types, key| - key = key.to_s.singularize - types = types.map { |t| t.to_s.singularize.camelize } - { "#{key}_ids".to_sym => ids, "#{key}_types".to_sym => types } - end - end - end - # ActionController methods and hooks for JSON API deserialization and # rendering. module Controller - extend ActiveSupport::Concern - - 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| - hash = controller.params.to_unsafe_hash[:_jsonapi] - if hash.nil? - JSONAPI::Rails.config[:logger].warn do - "Unable to deserialize #{key} because no JSON API payload was" \ - " found. (#{controller.controller_name}##{params[:action]})" - end - next - end - - ActiveSupport::Notifications - .instrument('parse.jsonapi-rails', - key: key, payload: hash, class: klass) do - JSONAPI::Parser::Resource.parse!(hash) - resource = klass.new(hash[:data]) - controller.request.env[JSONAPI_POINTERS_KEY] = - resource.reverse_mapping - controller.params[key.to_sym] = resource.to_hash - 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].dup - 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].dup - end - - # Hook for the jsonapi object. - # Overridden by the `jsonapi_object` renderer option. - # @return [Hash,nil] - def jsonapi_object - JSONAPI::Rails.config[:jsonapi_object] - end - - # Hook for default exposures. - # @return [Hash] - def jsonapi_expose - instance_exec(&JSONAPI::Rails.config[:jsonapi_expose]) - end - - # Hook for default cache. - # @return [#fetch_multi] - def jsonapi_cache - instance_exec(&JSONAPI::Rails.config[:jsonapi_cache]) - end - - # Hook for default fields. - # @return [Hash{Symbol=>Array},nil] - def jsonapi_fields - instance_exec(&JSONAPI::Rails.config[:jsonapi_fields]) - end - - # Hook for default includes. - # @return [IncludeDirective] - def jsonapi_include - instance_exec(&JSONAPI::Rails.config[:jsonapi_include]) - end - - # Hook for default links. - # @return [Hash] - def jsonapi_links - instance_exec(&JSONAPI::Rails.config[:jsonapi_links]) - end - - # Hook for default meta. - # @return [Hash,nil] - def jsonapi_meta - instance_exec(&JSONAPI::Rails.config[:jsonapi_meta]) - end - - # 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] || {} - end + include Deserialization + include Hooks end end end diff --git a/lib/jsonapi/rails/controller/deserialization.rb b/lib/jsonapi/rails/controller/deserialization.rb new file mode 100644 index 0000000..c89934e --- /dev/null +++ b/lib/jsonapi/rails/controller/deserialization.rb @@ -0,0 +1,83 @@ +require 'jsonapi/parser' +require 'jsonapi/rails/deserializable_resource' + +module JSONAPI + module Rails + module Controller + # Controller class and instance methods for deserialization of incoming + # JSON API payloads. + module Deserialization + extend ActiveSupport::Concern + + 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::DeserializableResource, &block) + + before_action(options) do |controller| + hash = controller.params.to_unsafe_hash[:_jsonapi] + if hash.nil? + JSONAPI::Rails.logger.warn do + "Unable to deserialize #{key} because no JSON API payload was" \ + " found. (#{controller.controller_name}##{params[:action]})" + end + next + end + + ActiveSupport::Notifications + .instrument('parse.jsonapi-rails', + key: key, payload: hash, class: klass) do + JSONAPI::Parser::Resource.parse!(hash) + resource = klass.new(hash[:data]) + controller.request.env[JSONAPI_POINTERS_KEY] = + resource.reverse_mapping + controller.params[key.to_sym] = resource.to_hash + end + end + end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize + end + + # JSON pointers for deserialized fields. + # @return [Hash{Symbol=>String}] + def jsonapi_pointers + request.env[JSONAPI_POINTERS_KEY] || {} + end + end + end + end +end diff --git a/lib/jsonapi/rails/controller/hooks.rb b/lib/jsonapi/rails/controller/hooks.rb new file mode 100644 index 0000000..d18a6c6 --- /dev/null +++ b/lib/jsonapi/rails/controller/hooks.rb @@ -0,0 +1,75 @@ +require 'jsonapi/rails/configuration' + +module JSONAPI + module Rails + module Controller + extend ActiveSupport::Concern + + # Hooks for customizing rendering default options at controller-level. + module Hooks + # 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].dup + 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].dup + end + + # Hook for the jsonapi object. + # Overridden by the `jsonapi_object` renderer option. + # @return [Hash,nil] + def jsonapi_object + JSONAPI::Rails.config[:jsonapi_object] + end + + # Hook for default exposures. + # @return [Hash] + def jsonapi_expose + instance_exec(&JSONAPI::Rails.config[:jsonapi_expose]) + end + + # Hook for default cache. + # @return [#fetch_multi] + def jsonapi_cache + instance_exec(&JSONAPI::Rails.config[:jsonapi_cache]) + end + + # Hook for default fields. + # @return [Hash{Symbol=>Array},nil] + def jsonapi_fields + instance_exec(&JSONAPI::Rails.config[:jsonapi_fields]) + end + + # Hook for default includes. + # @return [IncludeDirective] + def jsonapi_include + instance_exec(&JSONAPI::Rails.config[:jsonapi_include]) + end + + # Hook for default links. + # @return [Hash] + def jsonapi_links + instance_exec(&JSONAPI::Rails.config[:jsonapi_links]) + end + + # Hook for default meta. + # @return [Hash,nil] + def jsonapi_meta + instance_exec(&JSONAPI::Rails.config[:jsonapi_meta]) + end + + # Hook for pagination scheme. + # @return [Hash] + def jsonapi_pagination(resources) + instance_exec(resources, &JSONAPI::Rails.config[:jsonapi_pagination]) + end + end + end + end +end diff --git a/lib/jsonapi/rails/deserializable_resource.rb b/lib/jsonapi/rails/deserializable_resource.rb new file mode 100644 index 0000000..1761131 --- /dev/null +++ b/lib/jsonapi/rails/deserializable_resource.rb @@ -0,0 +1,21 @@ +require 'jsonapi/deserializable/resource' + +module JSONAPI + module Rails + # Customized deserializable resource class to match ActiveRecord's API. + class DeserializableResource < JSONAPI::Deserializable::Resource + id + type + attributes + has_one do |_rel, id, type, key| + type = type.to_s.singularize.camelize + { "#{key}_id".to_sym => id, "#{key}_type".to_sym => type } + end + has_many do |_rel, ids, types, key| + key = key.to_s.singularize + types = types.map { |t| t.to_s.singularize.camelize } + { "#{key}_ids".to_sym => ids, "#{key}_types".to_sym => types } + end + end + end +end diff --git a/lib/jsonapi/rails/log_subscriber.rb b/lib/jsonapi/rails/log_subscriber.rb index b5ef8c2..7587b7c 100644 --- a/lib/jsonapi/rails/log_subscriber.rb +++ b/lib/jsonapi/rails/log_subscriber.rb @@ -16,7 +16,7 @@ def parse(event) end def logger - JSONAPI::Rails.config[:logger] + JSONAPI::Rails.logger end end end diff --git a/lib/jsonapi/rails/logging.rb b/lib/jsonapi/rails/logging.rb new file mode 100644 index 0000000..e5ee6ad --- /dev/null +++ b/lib/jsonapi/rails/logging.rb @@ -0,0 +1,10 @@ +module JSONAPI + module Rails + # @private + module Logging + def logger + config[:logger] + end + end + end +end diff --git a/lib/jsonapi/rails/railtie.rb b/lib/jsonapi/rails/railtie.rb index 3a9ed17..9d083d8 100644 --- a/lib/jsonapi/rails/railtie.rb +++ b/lib/jsonapi/rails/railtie.rb @@ -1,6 +1,4 @@ require 'rails/railtie' -require 'action_controller' -require 'active_support' require 'jsonapi/rails/log_subscriber' require 'jsonapi/rails/renderer'