Skip to content

Commit f98b88b

Browse files
committed
Refactor + add configurable defaults.
1 parent 91be2fb commit f98b88b

File tree

11 files changed

+185
-125
lines changed

11 files changed

+185
-125
lines changed
Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,31 @@
11
JSONAPI::Rails.configure do |config|
2-
# config.register_mime_type = true
3-
# config.register_param_parser = true
4-
# config.register_renderers = true
5-
# config.extend_action_controller = true
2+
# # Set a default serializable class mapping.
3+
# config.jsonapi_class = Hash.new { |h, k|
4+
# names = k.to_s.split('::')
5+
# klass = names.pop
6+
# h[k] = [*names, "Serializable#{klass}"].join('::').safe_constantize
7+
# }
8+
#
9+
# # Set a default serializable class mapping for errors.
10+
# config.jsonapi_errors_class = Hash.new { |h, k|
11+
# names = k.to_s.split('::')
12+
# klass = names.pop
13+
# h[k] = [*names, "Serializable#{klass}"].join('::').safe_constantize
14+
# }.tap { |h|
15+
# h[:'ActiveModel::Errors'] = JSONAPI::Rails::SerializableActiveModelErrors
16+
# h[:Hash] = JSONAPI::Rails::SerializableErrorHash
17+
# }
18+
#
19+
# # Set a default JSON API object.
20+
# config.jsonapi_object = {
21+
# version: '1.0'
22+
# }
23+
#
24+
# # Set default exposures.
25+
# config.jsonapi_expose = {
26+
# url_helpers: ::Rails.application.routes.url_helpers
27+
# }
28+
#
29+
# # Set a default pagination scheme.
30+
# config.jsonapi_pagination = ->(_) { nil }
631
end

lib/jsonapi/rails/configuration.rb

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,37 @@
1+
require 'jsonapi/rails/serializable_active_model_errors'
2+
require 'jsonapi/rails/serializable_error_hash'
3+
14
module JSONAPI
25
module Rails
36
class Configuration < ActiveSupport::InheritableOptions; end
7+
DEFAULT_JSONAPI_CLASS = Hash.new do |h, k|
8+
names = k.to_s.split('::')
9+
klass = names.pop
10+
h[k] = [*names, "Serializable#{klass}"].join('::').safe_constantize
11+
end.freeze
12+
13+
DEFAULT_JSONAPI_ERRORS_CLASS = DEFAULT_JSONAPI_CLASS.dup.merge!(
14+
'ActiveModel::Errors'.to_sym =>
15+
JSONAPI::Rails::SerializableActiveModelErrors,
16+
Hash: JSONAPI::Rails::SerializableErrorHash
17+
).freeze
18+
19+
DEFAULT_JSONAPI_OBJECT = {
20+
version: '1.0'
21+
}.freeze
22+
23+
DEFAULT_JSONAPI_EXPOSE = {
24+
url_helpers: ::Rails.application.routes.url_helpers
25+
}.freeze
26+
27+
DEFAULT_JSONAPI_PAGINATION = ->(_) { nil }
428

529
DEFAULT_CONFIG = {
6-
register_parameter_parser: true,
7-
register_mime_type: true,
8-
register_renderers: true,
9-
extend_action_controller: true
30+
jsonapi_class: DEFAULT_JSONAPI_CLASS,
31+
jsonapi_errors_class: DEFAULT_JSONAPI_ERRORS_CLASS,
32+
jsonapi_object: DEFAULT_JSONAPI_OBJECT,
33+
jsonapi_expose: DEFAULT_JSONAPI_EXPOSE,
34+
jsonapi_pagination: DEFAULT_JSONAPI_PAGINATION
1035
}.freeze
1136

1237
def self.configure

lib/jsonapi/rails/controller.rb

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
require 'jsonapi/deserializable'
22
require 'jsonapi/parser'
3+
require 'jsonapi/rails/configuration'
34

45
module JSONAPI
56
module Rails
67
module Deserializable
8+
# @private
79
class Resource < JSONAPI::Deserializable::Resource
810
id
911
type
@@ -20,22 +22,56 @@ class Resource < JSONAPI::Deserializable::Resource
2022
end
2123
end
2224

25+
# ActionController methods and hooks for JSON API deserialization and
26+
# rendering.
2327
module Controller
2428
extend ActiveSupport::Concern
2529

26-
JSONAPI_POINTERS_KEY = 'jsonapi_deserializable.jsonapi_pointers'.freeze
30+
JSONAPI_POINTERS_KEY = 'jsonapi-rails.jsonapi_pointers'.freeze
2731

2832
class_methods do
33+
# Declare a deserializable resource.
34+
#
35+
# @param key [Symbol] The key under which the deserialized hash will be
36+
# available within the `params` hash.
37+
# @param options [Hash]
38+
# @option class [Class] A custom deserializer class. Optional.
39+
# @option only List of actions for which deserialization should happen.
40+
# Optional.
41+
# @option except List of actions for which deserialization should not
42+
# happen. Optional.
43+
# @yieldreturn Optional block for in-line definition of custom
44+
# deserializers.
45+
#
46+
# @example
47+
# class ArticlesController < ActionController::Base
48+
# deserializable_resource :article, only: [:create, :update]
49+
#
50+
# def create
51+
# article = Article.new(params[:article])
52+
#
53+
# if article.save
54+
# render jsonapi: article
55+
# else
56+
# render jsonapi_errors: article.errors
57+
# end
58+
# end
59+
#
60+
# # ...
61+
# end
62+
#
63+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize
2964
def deserializable_resource(key, options = {}, &block)
3065
options = options.dup
3166
klass = options.delete(:class) ||
3267
Class.new(JSONAPI::Rails::Deserializable::Resource, &block)
3368

3469
before_action(options) do |controller|
70+
# TODO(lucas): Fail with helpful error message if _jsonapi not
71+
# present.
3572
hash = controller.params[:_jsonapi].to_unsafe_hash
36-
ActiveSupport::Notifications.instrument('parse.jsonapi',
37-
payload: hash,
38-
class: klass) do
73+
ActiveSupport::Notifications
74+
.instrument('parse.jsonapi', payload: hash, class: klass) do
3975
JSONAPI::Parser::Resource.parse!(hash)
4076
resource = klass.new(hash[:data])
4177
controller.request.env[JSONAPI_POINTERS_KEY] =
@@ -44,34 +80,46 @@ def deserializable_resource(key, options = {}, &block)
4480
end
4581
end
4682
end
83+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
4784
end
4885

86+
# Hook for serializable class mapping (for resources).
87+
# Overridden by the `class` renderer option.
88+
# @return [Hash{Symbol=>Class}]
4989
def jsonapi_class
50-
# TODO(lucas): Make this configurable
51-
Hash.new do |h, k|
52-
names = k.to_s.split('::')
53-
klass = names.pop
54-
h[k] = [*names, "Serializable#{klass}"].join('::').safe_constantize
55-
end
90+
JSONAPI::Rails.config[:jsonapi_class]
91+
end
92+
93+
# Hook for serializable class mapping (for errors).
94+
# Overridden by the `class` renderer option.
95+
# @return [Hash{Symbol=>Class}]
96+
def jsonapi_errors_class
97+
JSONAPI::Rails.config[:jsonapi_errors_class]
5698
end
5799

100+
# Hook for the jsonapi object.
101+
# Overridden by the `jsonapi_object` renderer option.
102+
# @return [Hash]
58103
def jsonapi_object
59-
nil
104+
JSONAPI::Rails.config[:jsonapi_object]
60105
end
61106

107+
# Hook for default exposures.
108+
# @return [Hash]
62109
def jsonapi_expose
63-
# TODO(lucas): Make this configurable
64-
{
65-
url_helpers: ::Rails.application.routes.url_helpers
66-
}
110+
JSONAPI::Rails.config[:jsonapi_expose]
67111
end
68112

69-
def jsonapi_pagination(_collection)
70-
nil
113+
# Hook for pagination scheme.
114+
# @return [Hash]
115+
def jsonapi_pagination(resources)
116+
instance_exec(resources, &JSONAPI::Rails.config[:jsonapi_pagination])
71117
end
72118

119+
# JSON pointers for deserialized fields.
120+
# @return [Hash{Symbol=>String}]
73121
def jsonapi_pointers
74-
request.env[JSONAPI_POINTERS_KEY]
122+
request.env[JSONAPI_POINTERS_KEY] || {}
75123
end
76124
end
77125
end

lib/jsonapi/rails/parser.rb

Lines changed: 0 additions & 12 deletions
This file was deleted.

lib/jsonapi/rails/railtie.rb

Lines changed: 39 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,56 +2,66 @@
22
require 'action_controller'
33
require 'active_support'
44

5-
require 'jsonapi/rails/configuration'
6-
require 'jsonapi/rails/controller'
7-
require 'jsonapi/rails/parser'
85
require 'jsonapi/rails/renderer'
96

107
module JSONAPI
118
module Rails
9+
# @private
1210
class Railtie < ::Rails::Railtie
1311
MEDIA_TYPE = 'application/vnd.api+json'.freeze
12+
PARSER = lambda do |body|
13+
data = JSON.parse(body)
14+
hash = { _jsonapi: data }
15+
16+
hash.with_indifferent_access
17+
end
1418
RENDERERS = {
1519
jsonapi: SuccessRenderer.new,
1620
jsonapi_errors: ErrorsRenderer.new
1721
}.freeze
1822

19-
initializer 'jsonapi.init', after: :load_config_initializers do
20-
if JSONAPI::Rails.config.register_mime_type
21-
Mime::Type.register MEDIA_TYPE, :jsonapi
23+
initializer 'jsonapi-rails.init' do
24+
register_mime_type
25+
register_parameter_parser
26+
register_renderers
27+
ActiveSupport.on_load(:action_controller) do
28+
require 'jsonapi/rails/controller'
29+
include ::JSONAPI::Rails::Controller
2230
end
31+
end
2332

24-
if JSONAPI::Rails.config.register_parameter_parser
25-
if ::Rails::VERSION::MAJOR >= 5
26-
::ActionDispatch::Request.parameter_parsers[:jsonapi] = PARSER
27-
else
28-
::ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = PARSER
29-
end
30-
end
33+
private
3134

32-
if JSONAPI::Rails.config.extend_action_controller
33-
ActiveSupport.on_load(:action_controller) do
34-
include ::JSONAPI::Rails::Controller
35-
end
35+
def register_mime_type
36+
Mime::Type.register(MEDIA_TYPE, :jsonapi)
37+
end
38+
39+
def register_parameter_parser
40+
if ::Rails::VERSION::MAJOR >= 5
41+
ActionDispatch::Request.parameter_parsers[:jsonapi] = PARSER
42+
else
43+
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = PARSER
3644
end
45+
end
46+
47+
# rubocop:disable Metrics/MethodLength
48+
def register_renderers
49+
ActiveSupport.on_load(:action_controller) do
50+
RENDERERS.each do |name, renderer|
51+
::ActionController::Renderers.add(name) do |resources, options|
52+
# Renderer proc is evaluated in the controller context.
53+
self.content_type ||= Mime[:jsonapi]
3754

38-
if JSONAPI::Rails.config.register_renderers
39-
ActiveSupport.on_load(:action_controller) do
40-
RENDERERS.each do |name, renderer|
41-
::ActionController::Renderers.add(name) do |resources, options|
42-
# Renderer proc is evaluated in the controller context.
43-
self.content_type ||= Mime[:jsonapi]
44-
45-
ActiveSupport::Notifications.instrument('render.jsonapi',
46-
resources: resources,
47-
options: options) do
48-
renderer.render(resources, options, self).to_json
49-
end
55+
ActiveSupport::Notifications.instrument('render.jsonapi',
56+
resources: resources,
57+
options: options) do
58+
renderer.render(resources, options, self).to_json
5059
end
5160
end
5261
end
5362
end
5463
end
64+
# rubocop:enable Metrics/MethodLength
5565
end
5666
end
5767
end

lib/jsonapi/rails/renderer.rb

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
require 'jsonapi/rails/serializable_active_model_errors'
2-
require 'jsonapi/rails/serializable_error_hash'
31
require 'jsonapi/serializable/renderer'
42

53
module JSONAPI
64
module Rails
5+
# @private
76
class SuccessRenderer
87
def initialize(renderer = JSONAPI::Serializable::Renderer.new)
98
@renderer = renderer
@@ -19,19 +18,20 @@ def render(resources, options, controller)
1918

2019
private
2120

22-
# @api private
2321
def default_options(options, controller, resources)
2422
options.dup.tap do |opts|
2523
opts[:class] ||= controller.jsonapi_class
2624
if (pagination_links = controller.jsonapi_pagination(resources))
27-
(opts[:links] ||= {}).merge!(pagination_links)
25+
opts[:links] = (opts[:links] || {}).merge(pagination_links)
2826
end
29-
opts[:expose] = controller.jsonapi_expose.merge!(opts[:expose] || {})
30-
opts[:jsonapi] = opts.delete(:jsonapi_object) || controller.jsonapi_object
27+
opts[:expose] = controller.jsonapi_expose.merge(opts[:expose] || {})
28+
opts[:jsonapi] = opts.delete(:jsonapi_object) ||
29+
controller.jsonapi_object
3130
end
3231
end
3332
end
3433

34+
# @private
3535
class ErrorsRenderer
3636
def initialize(renderer = JSONAPI::Serializable::Renderer.new)
3737
@renderer = renderer
@@ -49,22 +49,15 @@ def render(errors, options, controller)
4949

5050
private
5151

52-
# @api private
5352
def default_options(options, controller)
5453
options.dup.tap do |opts|
55-
opts[:class] ||= controller.jsonapi_class
56-
unless opts[:class].key?(:'ActiveModel::Errors')
57-
opts[:class][:'ActiveModel::Errors'] =
58-
JSONAPI::Rails::SerializableActiveModelErrors
59-
end
60-
unless opts[:class].key?(:Hash)
61-
opts[:class][:Hash] = JSONAPI::Rails::SerializableErrorHash
62-
end
54+
opts[:class] ||= controller.jsonapi_errors_class
6355
opts[:expose] =
6456
controller.jsonapi_expose
65-
.merge!(opts[:expose] || {})
66-
.merge!(_jsonapi_pointers: controller.jsonapi_pointers)
67-
opts[:jsonapi] = opts[:jsonapi_object] || controller.jsonapi_object
57+
.merge(opts[:expose] || {})
58+
.merge!(_jsonapi_pointers: controller.jsonapi_pointers)
59+
opts[:jsonapi] = opts.delete(:jsonapi_object) ||
60+
controller.jsonapi_object
6861
end
6962
end
7063
end

lib/jsonapi/rails/serializable_active_model_errors.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
module JSONAPI
22
module Rails
3+
# @private
34
class SerializableActiveModelError < Serializable::Error
45
title do
56
"Invalid #{@field}" unless @field.nil?
@@ -14,6 +15,7 @@ class SerializableActiveModelError < Serializable::Error
1415
end
1516
end
1617

18+
# @private
1719
class SerializableActiveModelErrors
1820
def initialize(exposures)
1921
@errors = exposures[:object]

0 commit comments

Comments
 (0)