Skip to content

Commit 9cffc10

Browse files
committed
Add Rails >= 5.0.beta3 JSON API params parsing (#1751)
This reverts commit 6288203.
1 parent 6288203 commit 9cffc10

File tree

6 files changed

+213
-46
lines changed

6 files changed

+213
-46
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Features:
99
Fixes:
1010
- [#1710](https://github.com/rails-api/active_model_serializers/pull/1710) Prevent association loading when `include_data` option
1111
is set to `false`. (@groyoh)
12+
- [#1747](https://github.com/rails-api/active_model_serializers/pull/1747) Improve jsonapi mime type registration for Rails 5 (@remear)
1213

1314
Misc:
1415
- [#1734](https://github.com/rails-api/active_model_serializers/pull/1734) Adds documentation for conditional attribute (@lambda2)

lib/active_model_serializers/register_jsonapi_renderer.rb

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -22,43 +22,54 @@
2222
# render jsonapi: model
2323
#
2424
# No wrapper format needed as it does not apply (i.e. no `wrap_parameters format: [jsonapi]`)
25-
2625
module ActiveModelSerializers::Jsonapi
2726
MEDIA_TYPE = 'application/vnd.api+json'.freeze
2827
HEADERS = {
2928
response: { 'CONTENT_TYPE'.freeze => MEDIA_TYPE },
3029
request: { 'ACCEPT'.freeze => MEDIA_TYPE }
3130
}.freeze
31+
32+
def self.install
33+
# actionpack/lib/action_dispatch/http/mime_types.rb
34+
Mime::Type.register MEDIA_TYPE, :jsonapi
35+
36+
if Rails::VERSION::MAJOR >= 5
37+
ActionDispatch::Request.parameter_parsers[:jsonapi] = parser
38+
else
39+
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = parser
40+
end
41+
42+
# ref https://github.com/rails/rails/pull/21496
43+
ActionController::Renderers.add :jsonapi do |json, options|
44+
json = serialize_jsonapi(json, options).to_json(options) unless json.is_a?(String)
45+
self.content_type ||= Mime[:jsonapi]
46+
self.response_body = json
47+
end
48+
end
49+
50+
# Proposal: should actually deserialize the JSON API params
51+
# to the hash format expected by `ActiveModel::Serializers::JSON`
52+
# actionpack/lib/action_dispatch/http/parameters.rb
53+
def self.parser
54+
lambda do |body|
55+
data = JSON.parse(body)
56+
data = { :_json => data } unless data.is_a?(Hash)
57+
data.with_indifferent_access
58+
end
59+
end
60+
3261
module ControllerSupport
3362
def serialize_jsonapi(json, options)
3463
options[:adapter] = :json_api
35-
options.fetch(:serialization_context) { options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request) }
64+
options.fetch(:serialization_context) do
65+
options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request)
66+
end
3667
get_serializer(json, options)
3768
end
3869
end
3970
end
4071

41-
# actionpack/lib/action_dispatch/http/mime_types.rb
42-
Mime::Type.register ActiveModelSerializers::Jsonapi::MEDIA_TYPE, :jsonapi
43-
44-
parsers = Rails::VERSION::MAJOR >= 5 ? ActionDispatch::Http::Parameters : ActionDispatch::ParamsParser
45-
media_type = Mime::Type.lookup(ActiveModelSerializers::Jsonapi::MEDIA_TYPE)
46-
47-
# Proposal: should actually deserialize the JSON API params
48-
# to the hash format expected by `ActiveModel::Serializers::JSON`
49-
# actionpack/lib/action_dispatch/http/parameters.rb
50-
parsers::DEFAULT_PARSERS[media_type] = lambda do |body|
51-
data = JSON.parse(body)
52-
data = { :_json => data } unless data.is_a?(Hash)
53-
data.with_indifferent_access
54-
end
55-
56-
# ref https://github.com/rails/rails/pull/21496
57-
ActionController::Renderers.add :jsonapi do |json, options|
58-
json = serialize_jsonapi(json, options).to_json(options) unless json.is_a?(String)
59-
self.content_type ||= media_type
60-
self.response_body = json
61-
end
72+
ActiveModelSerializers::Jsonapi.install
6273

6374
ActiveSupport.on_load(:action_controller) do
6475
include ActiveModelSerializers::Jsonapi::ControllerSupport

test/action_controller/json_api/linked_test.rb

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
module ActionController
44
module Serialization
55
class JsonApi
6-
class LinkedTest < ActionController::TestCase
6+
class LinkedTest < ActionDispatch::IntegrationTest
77
class LinkedTestController < ActionController::Base
8-
require 'active_model_serializers/register_jsonapi_renderer'
98
def setup_post
109
ActionController::Base.cache_store.clear
1110
@role1 = Role.new(id: 1, name: 'admin')
@@ -39,70 +38,76 @@ def setup_post
3938

4039
def render_resource_without_include
4140
setup_post
42-
render jsonapi: @post
41+
render json: @post
4342
end
4443

4544
def render_resource_with_include
4645
setup_post
47-
render jsonapi: @post, include: [:author]
46+
render json: @post, adapter: :json_api, include: [:author]
4847
end
4948

5049
def render_resource_with_include_of_custom_key_by_original
5150
setup_post
52-
render jsonapi: @post, include: [:reviews], serializer: PostWithCustomKeysSerializer
51+
render json: @post, adapter: :json_api, include: [:reviews], serializer: PostWithCustomKeysSerializer
5352
end
5453

5554
def render_resource_with_nested_include
5655
setup_post
57-
render jsonapi: @post, include: [comments: [:author]]
56+
render json: @post, adapter: :json_api, include: [comments: [:author]]
5857
end
5958

6059
def render_resource_with_nested_has_many_include_wildcard
6160
setup_post
62-
render jsonapi: @post, include: 'author.*'
61+
render json: @post, adapter: :json_api, include: 'author.*'
6362
end
6463

6564
def render_resource_with_missing_nested_has_many_include
6665
setup_post
6766
@post.author = @author2 # author2 has no roles.
68-
render jsonapi: @post, include: [author: [:roles]]
67+
render json: @post, adapter: :json_api, include: [author: [:roles]]
6968
end
7069

7170
def render_collection_with_missing_nested_has_many_include
7271
setup_post
7372
@post.author = @author2
74-
render jsonapi: [@post, @post2], include: [author: [:roles]]
73+
render json: [@post, @post2], adapter: :json_api, include: [author: [:roles]]
7574
end
7675

7776
def render_collection_without_include
7877
setup_post
79-
render jsonapi: [@post]
78+
render json: [@post], adapter: :json_api
8079
end
8180

8281
def render_collection_with_include
8382
setup_post
84-
render jsonapi: [@post], include: 'author, comments'
83+
render json: [@post], adapter: :json_api, include: 'author, comments'
8584
end
8685
end
8786

88-
tests LinkedTestController
87+
setup do
88+
@routes = Rails.application.routes.draw do
89+
ActiveSupport::Deprecation.silence do
90+
match ':action', :to => LinkedTestController, via: [:get, :post]
91+
end
92+
end
93+
end
8994

9095
def test_render_resource_without_include
91-
get :render_resource_without_include
96+
get '/render_resource_without_include'
9297
response = JSON.parse(@response.body)
9398
refute response.key? 'included'
9499
end
95100

96101
def test_render_resource_with_include
97-
get :render_resource_with_include
102+
get '/render_resource_with_include'
98103
response = JSON.parse(@response.body)
99104
assert response.key? 'included'
100105
assert_equal 1, response['included'].size
101106
assert_equal 'Steve K.', response['included'].first['attributes']['name']
102107
end
103108

104109
def test_render_resource_with_nested_has_many_include
105-
get :render_resource_with_nested_has_many_include_wildcard
110+
get '/render_resource_with_nested_has_many_include_wildcard'
106111
response = JSON.parse(@response.body)
107112
expected_linked = [
108113
{
@@ -144,7 +149,7 @@ def test_render_resource_with_nested_has_many_include
144149
end
145150

146151
def test_render_resource_with_include_of_custom_key_by_original
147-
get :render_resource_with_include_of_custom_key_by_original
152+
get '/render_resource_with_include_of_custom_key_by_original'
148153
response = JSON.parse(@response.body)
149154
assert response.key? 'included'
150155

@@ -156,33 +161,33 @@ def test_render_resource_with_include_of_custom_key_by_original
156161
end
157162

158163
def test_render_resource_with_nested_include
159-
get :render_resource_with_nested_include
164+
get '/render_resource_with_nested_include'
160165
response = JSON.parse(@response.body)
161166
assert response.key? 'included'
162167
assert_equal 3, response['included'].size
163168
end
164169

165170
def test_render_collection_without_include
166-
get :render_collection_without_include
171+
get '/render_collection_without_include'
167172
response = JSON.parse(@response.body)
168173
refute response.key? 'included'
169174
end
170175

171176
def test_render_collection_with_include
172-
get :render_collection_with_include
177+
get '/render_collection_with_include'
173178
response = JSON.parse(@response.body)
174179
assert response.key? 'included'
175180
end
176181

177182
def test_render_resource_with_nested_attributes_even_when_missing_associations
178-
get :render_resource_with_missing_nested_has_many_include
183+
get '/render_resource_with_missing_nested_has_many_include'
179184
response = JSON.parse(@response.body)
180185
assert response.key? 'included'
181186
refute has_type?(response['included'], 'roles')
182187
end
183188

184189
def test_render_collection_with_missing_nested_has_many_include
185-
get :render_collection_with_missing_nested_has_many_include
190+
get '/render_collection_with_missing_nested_has_many_include'
186191
response = JSON.parse(@response.body)
187192
assert response.key? 'included'
188193
assert has_type?(response['included'], 'roles')
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
require 'support/isolated_unit'
2+
require 'minitest/mock'
3+
require 'action_dispatch'
4+
require 'action_controller'
5+
6+
class JsonApiRendererTest < ActionDispatch::IntegrationTest
7+
include ActiveSupport::Testing::Isolation
8+
9+
class TestController < ActionController::Base
10+
class << self
11+
attr_accessor :last_request_parameters
12+
end
13+
14+
def render_with_jsonapi_renderer
15+
author = Author.new(params[:data][:attributes])
16+
render jsonapi: author
17+
end
18+
19+
def parse
20+
self.class.last_request_parameters = request.request_parameters
21+
head :ok
22+
end
23+
end
24+
25+
def teardown
26+
TestController.last_request_parameters = nil
27+
end
28+
29+
def assert_parses(expected, actual, headers = {})
30+
post '/parse', params: actual, headers: headers
31+
assert_response :ok
32+
assert_equal(expected, TestController.last_request_parameters)
33+
end
34+
35+
class WithoutRenderer < JsonApiRendererTest
36+
setup do
37+
require 'rails'
38+
require 'active_record'
39+
require 'support/rails5_shims'
40+
require 'active_model_serializers'
41+
require 'fixtures/poro'
42+
43+
make_basic_app
44+
45+
Rails.application.routes.draw do
46+
ActiveSupport::Deprecation.silence do
47+
match ':action', :to => TestController, via: [:get, :post]
48+
end
49+
end
50+
end
51+
52+
def test_jsonapi_parser_not_registered
53+
parsers = if Rails::VERSION::MAJOR >= 5
54+
ActionDispatch::Request.parameter_parsers
55+
else
56+
ActionDispatch::ParamsParser::DEFAULT_PARSERS
57+
end
58+
assert_nil parsers[Mime[:jsonapi]]
59+
end
60+
61+
def test_jsonapi_renderer_not_registered
62+
expected = {
63+
'data' => {
64+
'attributes' => {
65+
'name' => 'Johnny Rico'
66+
},
67+
'type' => 'users'
68+
}
69+
}
70+
payload = '{"data": {"attributes": {"name": "Johnny Rico"}, "type": "authors"}}'
71+
headers = { 'CONTENT_TYPE' => 'application/vnd.api+json' }
72+
post '/render_with_jsonapi_renderer', params: payload, headers: headers
73+
assert expected, response.body
74+
end
75+
76+
def test_jsonapi_parser
77+
assert_parses(
78+
{},
79+
'',
80+
'CONTENT_TYPE' => 'application/vnd.api+json'
81+
)
82+
end
83+
end
84+
85+
class WithRenderer < JsonApiRendererTest
86+
setup do
87+
require 'rails'
88+
require 'active_record'
89+
require 'support/rails5_shims'
90+
require 'active_model_serializers'
91+
require 'fixtures/poro'
92+
require 'active_model_serializers/register_jsonapi_renderer'
93+
94+
make_basic_app
95+
96+
Rails.application.routes.draw do
97+
ActiveSupport::Deprecation.silence do
98+
match ':action', :to => TestController, via: [:get, :post]
99+
end
100+
end
101+
end
102+
103+
def test_jsonapi_parser_registered
104+
if Rails::VERSION::MAJOR >= 5
105+
parsers = ActionDispatch::Request.parameter_parsers
106+
assert_equal Proc, parsers[:jsonapi].class
107+
else
108+
parsers = ActionDispatch::ParamsParser::DEFAULT_PARSERS
109+
assert_equal Proc, parsers[Mime[:jsonapi]].class
110+
end
111+
end
112+
113+
def test_jsonapi_renderer_registered
114+
expected = {
115+
'data' => {
116+
'attributes' => {
117+
'name' => 'Johnny Rico'
118+
},
119+
'type' => 'users'
120+
}
121+
}
122+
payload = '{"data": {"attributes": {"name": "Johnny Rico"}, "type": "authors"}}'
123+
headers = { 'CONTENT_TYPE' => 'application/vnd.api+json' }
124+
post '/render_with_jsonapi_renderer', params: payload, headers: headers
125+
assert expected, response.body
126+
end
127+
128+
def test_jsonapi_parser
129+
assert_parses(
130+
{
131+
'data' => {
132+
'attributes' => {
133+
'name' => 'John Doe'
134+
},
135+
'type' => 'users'
136+
}
137+
},
138+
'{"data": {"attributes": {"name": "John Doe"}, "type": "users"}}',
139+
'CONTENT_TYPE' => 'application/vnd.api+json'
140+
)
141+
end
142+
end
143+
end

test/support/isolated_unit.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141

4242
# These files do not require any others and are needed
4343
# to run the tests
44+
require 'active_support/testing/autorun'
4445
require 'active_support/testing/isolation'
4546

4647
module TestHelpers

0 commit comments

Comments
 (0)