Skip to content

Commit daadf98

Browse files
committed
Provide key case translation
1 parent 31a30c8 commit daadf98

File tree

9 files changed

+311
-8
lines changed

9 files changed

+311
-8
lines changed

lib/action_controller/serialization.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ def use_adapter?
5656

5757
[:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
5858
define_method renderer_method do |resource, options|
59-
options.fetch(:serialization_context) { options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request) }
59+
options.fetch(:serialization_context) {
60+
options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request, options)
61+
}
6062
serializable_resource = get_serializer(resource, options)
6163
super(serializable_resource, options)
6264
end

lib/active_model_serializers/adapter/base.rb

+12
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,18 @@ def include_meta(json)
5151
json[meta_key] = meta if meta
5252
json
5353
end
54+
55+
def translate_key_casing!(value, serialization_context)
56+
return value unless serialization_context
57+
case serialization_context.key_case
58+
when :camel
59+
value.deep_transform_keys! { |key| key.to_s.camelize.to_sym }
60+
when :camel_lower
61+
value.deep_transform_keys! { |key| key.to_s.camelize(:lower).to_sym }
62+
else
63+
value
64+
end
65+
end
5466
end
5567
end
5668
end

lib/active_model_serializers/adapter/json.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ class Json < Base
66

77
def serializable_hash(options = nil)
88
options ||= {}
9-
{ root => Attributes.new(serializer, instance_options).serializable_hash(options) }
9+
serialized_hash = { root => Attributes.new(serializer, instance_options).serializable_hash(options) }
10+
translate_key_casing!(serialized_hash, options[:serialization_context])
1011
end
1112

1213
private

lib/active_model_serializers/adapter/json_api.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,12 @@ def initialize(serializer, options = {})
2222
# {http://jsonapi.org/format/#document-top-level data and errors MUST NOT coexist in the same document.}
2323
def serializable_hash(options = nil)
2424
options ||= {}
25-
if serializer.success?
25+
document = if serializer.success?
2626
success_document(options)
2727
else
2828
failure_document
2929
end
30+
translate_key_casing!(document, options[:serialization_context])
3031
end
3132

3233
# {http://jsonapi.org/format/#document-top-level Primary data}

lib/active_model_serializers/serialization_context.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,14 @@ class << self
44
attr_writer :url_helpers, :default_url_options
55
end
66

7-
attr_reader :request_url, :query_parameters
7+
attr_reader :request_url, :query_parameters, :key_case
88

99
def initialize(request, options = {})
1010
@request_url = request.original_url[/\A[^?]+/]
1111
@query_parameters = request.query_parameters
1212
@url_helpers = options.delete(:url_helpers) || self.class.url_helpers
1313
@default_url_options = options.delete(:default_url_options) || self.class.default_url_options
14+
@key_case = options.delete(:key_case) || :default
1415
end
1516

1617
def self.url_helpers

test/adapter/json/key_case_test.rb

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
require 'test_helper'
2+
3+
module ActiveModelSerializers
4+
module Adapter
5+
class Json
6+
class KeyCaseTest < ActiveSupport::TestCase
7+
def mock_request(key_case)
8+
context = Minitest::Mock.new
9+
context.expect(:request_url, URI)
10+
context.expect(:query_parameters, {})
11+
context.expect(:key_case, key_case)
12+
@options = {}
13+
@options[:serialization_context] = context
14+
end
15+
16+
Post = Class.new(::Model)
17+
class PostSerializer < ActiveModel::Serializer
18+
attributes :id, :title, :body, :publish_at
19+
end
20+
21+
def setup
22+
ActionController::Base.cache_store.clear
23+
@blog = Blog.new(id: 1, name: 'My Blog!!', special_attribute: 'neat')
24+
serializer = CustomBlogSerializer.new(@blog)
25+
@adapter = ActiveModelSerializers::Adapter::Json.new(serializer)
26+
end
27+
28+
def test_key_case_default
29+
mock_request(:default)
30+
assert_equal({
31+
blog: { id: 1, special_attribute: "neat", articles: nil }
32+
}, @adapter.serializable_hash(@options))
33+
end
34+
35+
def test_key_case_camel
36+
mock_request(:camel)
37+
assert_equal({
38+
Blog: { Id: 1, SpecialAttribute: "neat", Articles: nil }
39+
}, @adapter.serializable_hash(@options))
40+
end
41+
42+
def test_key_case_camel_lower
43+
mock_request(:camel_lower)
44+
assert_equal({
45+
blog: { id: 1, specialAttribute: "neat", articles: nil }
46+
}, @adapter.serializable_hash(@options))
47+
end
48+
end
49+
end
50+
end
51+
end
+234
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
require 'test_helper'
2+
3+
module ActiveModelSerializers
4+
module Adapter
5+
class JsonApi
6+
class KeyCaseTest < ActiveSupport::TestCase
7+
Post = Class.new(::Model)
8+
class PostSerializer < ActiveModel::Serializer
9+
type 'posts'
10+
attributes :title, :body, :publish_at
11+
belongs_to :author
12+
has_many :comments
13+
14+
link(:self) { post_url(object.id) }
15+
link(:post_authors) { post_authors_url(object.id) }
16+
link(:subscriber_comments) { post_comments_url(object.id) }
17+
18+
meta do
19+
{
20+
rating: 5,
21+
favorite_count: 10
22+
}
23+
end
24+
end
25+
26+
Author = Class.new(::Model)
27+
class AuthorSerializer < ActiveModel::Serializer
28+
type 'authors'
29+
attributes :first_name, :last_name
30+
end
31+
32+
Comment = Class.new(::Model)
33+
class CommentSerializer < ActiveModel::Serializer
34+
type 'comments'
35+
attributes :body
36+
belongs_to :author
37+
end
38+
39+
def mock_request(key_case = :default)
40+
context = Minitest::Mock.new
41+
context.expect(:request_url, URI)
42+
context.expect(:query_parameters, {})
43+
context.expect(:key_case, key_case)
44+
context.expect(:url_helpers, Rails.application.routes.url_helpers)
45+
@options = {}
46+
@options[:serialization_context] = context
47+
end
48+
49+
def setup
50+
Rails.application.routes.draw do
51+
resources :posts do
52+
resources :authors
53+
resources :comments
54+
end
55+
end
56+
@publish_at = 1.day.from_now
57+
@author = Author.new(id: 1, first_name: 'Bob', last_name: 'Jones')
58+
@comment1 = Comment.new(id: 7, body: 'cool', author: @author)
59+
@comment2 = Comment.new(id: 12, body: 'awesome', author: @author)
60+
@post = Post.new(id: 1337, title: 'Title 1', body: 'Body 1',
61+
author: @author, comments: [@comment1, @comment2],
62+
publish_at: @publish_at)
63+
@comment1.post = @post
64+
@comment2.post = @post
65+
end
66+
67+
def test_success_key_case_default
68+
mock_request
69+
serializer = PostSerializer.new(@post)
70+
adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer)
71+
result = adapter.serializable_hash(@options)
72+
assert_equal({
73+
data: {
74+
id: "1337",
75+
type: "posts",
76+
attributes: {
77+
title: "Title 1",
78+
body: "Body 1",
79+
publish_at: @publish_at
80+
},
81+
relationships: {
82+
author: {
83+
data: { id: "1", type: "authors" }
84+
},
85+
comments: {
86+
data: [
87+
{ id: "7", type: "comments" },
88+
{ id: "12", type: "comments" }
89+
]}
90+
},
91+
links: {
92+
self: "http://example.com/posts/1337",
93+
post_authors: "http://example.com/posts/1337/authors",
94+
subscriber_comments: "http://example.com/posts/1337/comments"
95+
},
96+
meta: { rating: 5, favorite_count: 10 }
97+
}
98+
}, result)
99+
end
100+
101+
def test_success_key_case_camel
102+
mock_request(:camel)
103+
serializer = PostSerializer.new(@post)
104+
adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer)
105+
result = adapter.serializable_hash(@options)
106+
assert_equal({
107+
Data: {
108+
Id: "1337",
109+
Type: "posts",
110+
Attributes: {
111+
Title: "Title 1",
112+
Body: "Body 1",
113+
PublishAt: @publish_at
114+
},
115+
Relationships: {
116+
Author: {
117+
Data: { Id: "1", Type: "authors" }
118+
},
119+
Comments: {
120+
Data: [
121+
{ Id: "7", Type: "comments" },
122+
{ Id: "12", Type: "comments" }
123+
]}
124+
},
125+
Links: {
126+
Self: "http://example.com/posts/1337",
127+
PostAuthors: "http://example.com/posts/1337/authors",
128+
SubscriberComments: "http://example.com/posts/1337/comments"
129+
},
130+
Meta: { Rating: 5, FavoriteCount: 10 }
131+
}
132+
}, result)
133+
end
134+
135+
def test_success_key_case_camel_lower
136+
mock_request(:camel_lower)
137+
serializer = PostSerializer.new(@post)
138+
adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer)
139+
result = adapter.serializable_hash(@options)
140+
assert_equal({
141+
data: {
142+
id: "1337",
143+
type: "posts",
144+
attributes: {
145+
title: "Title 1",
146+
body: "Body 1",
147+
publishAt: @publish_at
148+
},
149+
relationships: {
150+
author: {
151+
data: { id: "1", type: "authors" }
152+
},
153+
comments: {
154+
data: [
155+
{ id: "7", type: "comments" },
156+
{ id: "12", type: "comments" }
157+
]}
158+
},
159+
links: {
160+
self: "http://example.com/posts/1337",
161+
postAuthors: "http://example.com/posts/1337/authors",
162+
subscriberComments: "http://example.com/posts/1337/comments"
163+
},
164+
meta: { rating: 5, favoriteCount: 10 }
165+
}
166+
}, result)
167+
end
168+
169+
def test_error_document_key_case_default
170+
mock_request(:default)
171+
172+
resource = ModelWithErrors.new
173+
resource.errors.add(:published_at, 'must be in the future')
174+
resource.errors.add(:title, 'must be longer')
175+
176+
serializer = ActiveModel::Serializer::ErrorSerializer.new(resource)
177+
adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer)
178+
result = adapter.serializable_hash(@options)
179+
180+
expected_errors_object =
181+
{ :errors =>
182+
[
183+
{ :source => { :pointer => '/data/attributes/published_at' }, :detail => 'must be in the future' },
184+
{ :source => { :pointer => '/data/attributes/title' }, :detail => 'must be longer' }
185+
]
186+
}
187+
assert_equal expected_errors_object, result
188+
end
189+
190+
def test_error_document_key_case_camel
191+
mock_request(:camel)
192+
193+
resource = ModelWithErrors.new
194+
resource.errors.add(:published_at, 'must be in the future')
195+
resource.errors.add(:title, 'must be longer')
196+
197+
serializer = ActiveModel::Serializer::ErrorSerializer.new(resource)
198+
adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer)
199+
result = adapter.serializable_hash(@options)
200+
201+
expected_errors_object =
202+
{ :Errors =>
203+
[
204+
{ :Source => { :Pointer => '/data/attributes/published_at' }, :Detail => 'must be in the future' },
205+
{ :Source => { :Pointer => '/data/attributes/title' }, :Detail => 'must be longer' }
206+
]
207+
}
208+
assert_equal expected_errors_object, result
209+
end
210+
211+
def test_error_document_key_case_camel_lower
212+
mock_request(:camel_lower)
213+
214+
resource = ModelWithErrors.new
215+
resource.errors.add(:published_at, 'must be in the future')
216+
resource.errors.add(:title, 'must be longer')
217+
218+
serializer = ActiveModel::Serializer::ErrorSerializer.new(resource)
219+
adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer)
220+
result = adapter.serializable_hash(@options)
221+
222+
expected_errors_object =
223+
{ :errors =>
224+
[
225+
{ :source => { :pointer => '/data/attributes/published_at' }, :detail => 'must be in the future' },
226+
{ :source => { :pointer => '/data/attributes/title' }, :detail => 'must be longer' }
227+
]
228+
}
229+
assert_equal expected_errors_object, result
230+
end
231+
end
232+
end
233+
end
234+
end

test/adapter/json_api/links_test.rb

+4-4
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ def test_toplevel_links
4040
stuff: 'value'
4141
}
4242
}
43-
}).serializable_hash
43+
}).serializable_hash(@options)
4444
expected = {
4545
self: {
4646
href: 'http://example.com/posts',
@@ -57,7 +57,7 @@ def test_nil_toplevel_links
5757
@post,
5858
adapter: :json_api,
5959
links: nil
60-
).serializable_hash
60+
).serializable_hash(@options)
6161
refute hash.key?(:links), 'No links key to be output'
6262
end
6363

@@ -66,12 +66,12 @@ def test_nil_toplevel_links_json_adapter
6666
@post,
6767
adapter: :json,
6868
links: nil
69-
).serializable_hash
69+
).serializable_hash(@options)
7070
refute hash.key?(:links), 'No links key to be output'
7171
end
7272

7373
def test_resource_links
74-
hash = serializable(@author, adapter: :json_api).serializable_hash
74+
hash = serializable(@author, adapter: :json_api).serializable_hash(@options)
7575
expected = {
7676
self: {
7777
href: 'http://example.com/link_author/1337',

test/adapter/json_api/pagination_links_test.rb

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def mock_request(query_parameters = {}, original_url = URI)
2323
context = Minitest::Mock.new
2424
context.expect(:request_url, original_url)
2525
context.expect(:query_parameters, query_parameters)
26+
context.expect(:key_case, :default)
2627
@options = {}
2728
@options[:serialization_context] = context
2829
end

0 commit comments

Comments
 (0)