Skip to content

Commit 0f31cda

Browse files
authored
Refactor rendering + better errors support + configuration hooks. (#22)
* Refactor * Add support for ActiveModel::Errors. * Add configurable defaults.
1 parent 31df80d commit 0f31cda

14 files changed

+501
-270
lines changed

jsonapi-rails.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
1414
spec.files = Dir['README.md', 'lib/**/*']
1515
spec.require_path = 'lib'
1616

17-
spec.add_dependency 'jsonapi-rb', '~> 0.3.0'
17+
spec.add_dependency 'jsonapi-rb', '~> 0.5.0'
1818
spec.add_dependency 'jsonapi-parser', '~> 0.1.0'
1919

2020
spec.add_development_dependency 'rails', '~> 5.0'
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'.to_sym => 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: 69 additions & 11 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,24 +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}]
89+
def jsonapi_class
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]
98+
end
99+
100+
# Hook for the jsonapi object.
101+
# Overridden by the `jsonapi_object` renderer option.
102+
# @return [Hash]
49103
def jsonapi_object
50-
nil
104+
JSONAPI::Rails.config[:jsonapi_object]
51105
end
52106

107+
# Hook for default exposures.
108+
# @return [Hash]
53109
def jsonapi_expose
54-
{
55-
url_helpers: ::Rails.application.routes.url_helpers
56-
}
110+
JSONAPI::Rails.config[:jsonapi_expose]
57111
end
58112

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

119+
# JSON pointers for deserialized fields.
120+
# @return [Hash{Symbol=>String}]
63121
def jsonapi_pointers
64-
request.env[JSONAPI_POINTERS_KEY]
122+
request.env[JSONAPI_POINTERS_KEY] || {}
65123
end
66124
end
67125
end

lib/jsonapi/rails/parser.rb

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

lib/jsonapi/rails/railtie.rb

Lines changed: 41 additions & 31 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 = {
15-
jsonapi: SuccessRenderer.new,
16-
jsonapi_error: ErrorsRenderer.new
19+
jsonapi: SuccessRenderer.new,
20+
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

0 commit comments

Comments
 (0)