Skip to content

Commit 761da5c

Browse files
authored
Improve key formatting for nested hashes and disable by default (#497)
* Fix changing key format in child elements Since #486, key format is applied deeply to hashes and arrays that are passed to `set!`, `merge!` and `array!`: json.key_format! :upcase json.set!(:foo, {some: "value"}) # => {"FOO": {"SOME": "value"}} json.key_format! :upcase json.merge!({some: "value"}) # => {"SOME": "value"} json.key_format! :upcase json.array!([{some: "value"}]) # => [{"SOME": "value"}] This also works for arrays and hashes extracted from objects: comment = Struct.new(:author).new({ first_name: 'John', last_name: 'Doe' }) json.key_format! camlize: :lower json.set!(:comment, comment, :author) # => {"comment": {"author": {"firstName": "John", "lastName": "Doe"}}} As a side effect of the change, key format is also applied to the result of nested blocks, making it impossible to change key format in the scope of a block: json.key_format! camelize: :lower json.level_one do json.key_format! :upcase json.value 'two' end # => jbuilder 2.10.0: {"levelOne": {"VALUE": "two"}} # => jbuilder 2.11.0: {"levelOne": {"vALUE": "two"}} The string "vALUE" results from calling `"value".upcase.camelize(:lower)`. The same happens when trying to change the key format inside of an `array!` block. This happens since key transformation was added in the `_merge_values` method, which is used both by `merge!` but also when `set!` is called with a block. To restore the previous behavior, we pull the `_transform_keys` call up into `merge!` itself. To make sure extracted hashes and arrays keep being transformed (see comment/author example above), we apply `_transform_keys` in `_extract_hash_values` and `_extract_method_values`. This also aligns the behavior of `extract!` which was left out in #486: comment = {author: { first_name: 'John', last_name: 'Doe' }} result = jbuild do |json| json.key_format! camelize: :lower json.extract! comment, :author end # => jbuilder 2.10 and 2.11: {"author": { :first_name => "John", :last_name => "Doe" }} # => now: {"author": { "firstName": "John", "lastName": "Doe" }} Finally, to fix `array!`, we make it call `_merge_values` directly instead of relying on `merge!`. `array!` then has to transform keys itself when a collection is passed to preserve the new behavior introduced by #486. * Disable deeply formatting keys of nested hashes by default Since #486, key format was also applied to nested hashes that are passed as values: json.key_format! camelize: :lower json.settings({some_value: "abc"}) # => { "settings": { "someValue": "abc" }} This breaks code that relied on the previous behavior. Add a `deep_format_keys!` directive that can be used to opt into this new behavior: json.key_format! camelize: :lower json.deep_format_keys! json.settings({some_value: "abc"}) # => { "settings": { "someValue": "abc" }}
1 parent 18c06fe commit 761da5c

File tree

3 files changed

+196
-15
lines changed

3 files changed

+196
-15
lines changed

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,6 +274,25 @@ environment.rb for example):
274274
Jbuilder.key_format camelize: :lower
275275
```
276276

277+
By default, key format is not applied to keys of hashes that are
278+
passed to methods like `set!`, `array!` or `merge!`. You can opt into
279+
deeply transforming these as well:
280+
281+
``` ruby
282+
json.key_format! camelize: :lower
283+
json.deep_format_keys!
284+
json.settings([{some_value: "abc"}])
285+
286+
# => { "settings": [{ "someValue": "abc" }]}
287+
```
288+
289+
You can set this globally with the class method `deep_format_keys` (from inside your
290+
environment.rb for example):
291+
292+
``` ruby
293+
Jbuilder.deep_format_keys true
294+
```
295+
277296
## Contributing to Jbuilder
278297

279298
Jbuilder is the work of many contributors. You're encouraged to submit pull requests, propose

lib/jbuilder.rb

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
class Jbuilder
1010
@@key_formatter = nil
1111
@@ignore_nil = false
12+
@@deep_format_keys = false
1213

1314
def initialize(options = {})
1415
@attributes = {}
1516

1617
@key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil}
1718
@ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
19+
@deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys)
1820

1921
yield self if ::Kernel.block_given?
2022
end
@@ -131,6 +133,31 @@ def self.ignore_nil(value = true)
131133
@@ignore_nil = value
132134
end
133135

136+
# Deeply apply key format to nested hashes and arrays passed to
137+
# methods like set!, merge! or array!.
138+
#
139+
# Example:
140+
#
141+
# json.key_format! camelize: :lower
142+
# json.settings({some_value: "abc"})
143+
#
144+
# { "settings": { "some_value": "abc" }}
145+
#
146+
# json.key_format! camelize: :lower
147+
# json.deep_format_keys!
148+
# json.settings({some_value: "abc"})
149+
#
150+
# { "settings": { "someValue": "abc" }}
151+
#
152+
def deep_format_keys!(value = true)
153+
@deep_format_keys = value
154+
end
155+
156+
# Same as instance method deep_format_keys! except sets the default.
157+
def self.deep_format_keys(value = true)
158+
@@deep_format_keys = value
159+
end
160+
134161
# Turns the current element into an array and yields a builder to add a hash.
135162
#
136163
# Example:
@@ -190,10 +217,10 @@ def array!(collection = [], *attributes, &block)
190217
elsif attributes.any?
191218
_map_collection(collection) { |element| extract! element, *attributes }
192219
else
193-
collection.to_a
220+
_format_keys(collection.to_a)
194221
end
195222

196-
merge! array
223+
@attributes = _merge_values(@attributes, array)
197224
end
198225

199226
# Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
@@ -244,7 +271,7 @@ def attributes!
244271
# Merges hash, array, or Jbuilder instance into current builder.
245272
def merge!(object)
246273
hash_or_array = ::Jbuilder === object ? object.attributes! : object
247-
@attributes = _merge_values(@attributes, hash_or_array)
274+
@attributes = _merge_values(@attributes, _format_keys(hash_or_array))
248275
end
249276

250277
# Encodes the current builder as JSON.
@@ -255,11 +282,11 @@ def target!
255282
private
256283

257284
def _extract_hash_values(object, attributes)
258-
attributes.each{ |key| _set_value key, object.fetch(key) }
285+
attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
259286
end
260287

261288
def _extract_method_values(object, attributes)
262-
attributes.each{ |key| _set_value key, object.public_send(key) }
289+
attributes.each{ |key| _set_value key, _format_keys(object.public_send(key)) }
263290
end
264291

265292
def _merge_block(key)
@@ -273,11 +300,11 @@ def _merge_values(current_value, updates)
273300
if _blank?(updates)
274301
current_value
275302
elsif _blank?(current_value) || updates.nil? || current_value.empty? && ::Array === updates
276-
_format_keys(updates)
303+
updates
277304
elsif ::Array === current_value && ::Array === updates
278-
current_value + _format_keys(updates)
305+
current_value + updates
279306
elsif ::Hash === current_value && ::Hash === updates
280-
current_value.deep_merge(_format_keys(updates))
307+
current_value.deep_merge(updates)
281308
else
282309
raise MergeError.build(current_value, updates)
283310
end
@@ -288,6 +315,8 @@ def _key(key)
288315
end
289316

290317
def _format_keys(hash_or_array)
318+
return hash_or_array unless @deep_format_keys
319+
291320
if ::Array === hash_or_array
292321
hash_or_array.map { |value| _format_keys(value) }
293322
elsif ::Hash === hash_or_array
@@ -312,12 +341,12 @@ def _map_collection(collection)
312341
end
313342

314343
def _scope
315-
parent_attributes, parent_formatter = @attributes, @key_formatter
344+
parent_attributes, parent_formatter, parent_deep_format_keys = @attributes, @key_formatter, @deep_format_keys
316345
@attributes = BLANK
317346
yield
318347
@attributes
319348
ensure
320-
@attributes, @key_formatter = parent_attributes, parent_formatter
349+
@attributes, @key_formatter, @deep_format_keys = parent_attributes, parent_formatter, parent_deep_format_keys
321350
end
322351

323352
def _is_collection?(object)

test/jbuilder_test.rb

Lines changed: 138 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,36 @@ class JbuilderTest < ActiveSupport::TestCase
566566
assert_equal 'one', result['level1']
567567
end
568568

569+
test 'key_format! can be changed in child elements' do
570+
result = jbuild do |json|
571+
json.key_format! camelize: :lower
572+
573+
json.level_one do
574+
json.key_format! :upcase
575+
json.value 'two'
576+
end
577+
end
578+
579+
assert_equal ['levelOne'], result.keys
580+
assert_equal ['VALUE'], result['levelOne'].keys
581+
end
582+
583+
test 'key_format! can be changed in array!' do
584+
result = jbuild do |json|
585+
json.key_format! camelize: :lower
586+
587+
json.level_one do
588+
json.array! [{value: 'two'}] do |object|
589+
json.key_format! :upcase
590+
json.value object[:value]
591+
end
592+
end
593+
end
594+
595+
assert_equal ['levelOne'], result.keys
596+
assert_equal ['VALUE'], result['levelOne'][0].keys
597+
end
598+
569599
test 'key_format! with no parameter' do
570600
result = jbuild do |json|
571601
json.key_format! :upcase
@@ -593,58 +623,161 @@ class JbuilderTest < ActiveSupport::TestCase
593623
assert_equal ['oats and friends'], result.keys
594624
end
595625

596-
test 'key_format! with merge!' do
626+
test 'key_format! is not applied deeply by default' do
627+
names = { first_name: 'camel', last_name: 'case' }
628+
result = jbuild do |json|
629+
json.key_format! camelize: :lower
630+
json.set! :all_names, names
631+
end
632+
633+
assert_equal %i[first_name last_name], result['allNames'].keys
634+
end
635+
636+
test 'applying key_format! deeply can be enabled per scope' do
637+
names = { first_name: 'camel', last_name: 'case' }
638+
result = jbuild do |json|
639+
json.key_format! camelize: :lower
640+
json.scope do
641+
json.deep_format_keys!
642+
json.set! :all_names, names
643+
end
644+
json.set! :all_names, names
645+
end
646+
647+
assert_equal %w[firstName lastName], result['scope']['allNames'].keys
648+
assert_equal %i[first_name last_name], result['allNames'].keys
649+
end
650+
651+
test 'applying key_format! deeply can be disabled per scope' do
652+
names = { first_name: 'camel', last_name: 'case' }
653+
result = jbuild do |json|
654+
json.key_format! camelize: :lower
655+
json.deep_format_keys!
656+
json.set! :all_names, names
657+
json.scope do
658+
json.deep_format_keys! false
659+
json.set! :all_names, names
660+
end
661+
end
662+
663+
assert_equal %w[firstName lastName], result['allNames'].keys
664+
assert_equal %i[first_name last_name], result['scope']['allNames'].keys
665+
end
666+
667+
test 'applying key_format! deeply can be enabled globally' do
668+
names = { first_name: 'camel', last_name: 'case' }
669+
670+
Jbuilder.deep_format_keys true
671+
result = jbuild do |json|
672+
json.key_format! camelize: :lower
673+
json.set! :all_names, names
674+
end
675+
676+
assert_equal %w[firstName lastName], result['allNames'].keys
677+
Jbuilder.send(:class_variable_set, '@@deep_format_keys', false)
678+
end
679+
680+
test 'deep key_format! with merge!' do
597681
hash = { camel_style: 'for JS' }
598682
result = jbuild do |json|
599683
json.key_format! camelize: :lower
684+
json.deep_format_keys!
600685
json.merge! hash
601686
end
602687

603688
assert_equal ['camelStyle'], result.keys
604689
end
605690

606-
test 'key_format! with merge! deep' do
691+
test 'deep key_format! with merge! deep' do
607692
hash = { camel_style: { sub_attr: 'for JS' } }
608693
result = jbuild do |json|
609694
json.key_format! camelize: :lower
695+
json.deep_format_keys!
610696
json.merge! hash
611697
end
612698

613699
assert_equal ['subAttr'], result['camelStyle'].keys
614700
end
615701

616-
test 'key_format! with set! array of hashes' do
702+
test 'deep key_format! with set! array of hashes' do
617703
names = [{ first_name: 'camel', last_name: 'case' }]
618704
result = jbuild do |json|
619705
json.key_format! camelize: :lower
706+
json.deep_format_keys!
620707
json.set! :names, names
621708
end
622709

623710
assert_equal %w[firstName lastName], result['names'][0].keys
624711
end
625712

626-
test 'key_format! with array! of hashes' do
713+
test 'deep key_format! with set! extracting hash from object' do
714+
comment = Struct.new(:author).new({ first_name: 'camel', last_name: 'case' })
715+
result = jbuild do |json|
716+
json.key_format! camelize: :lower
717+
json.deep_format_keys!
718+
json.set! :comment, comment, :author
719+
end
720+
721+
assert_equal %w[firstName lastName], result['comment']['author'].keys
722+
end
723+
724+
test 'deep key_format! with array! of hashes' do
627725
names = [{ first_name: 'camel', last_name: 'case' }]
628726
result = jbuild do |json|
629727
json.key_format! camelize: :lower
728+
json.deep_format_keys!
630729
json.array! names
631730
end
632731

633732
assert_equal %w[firstName lastName], result[0].keys
634733
end
635734

636-
test 'key_format! with merge! array of hashes' do
735+
test 'deep key_format! with merge! array of hashes' do
637736
names = [{ first_name: 'camel', last_name: 'case' }]
638737
new_names = [{ first_name: 'snake', last_name: 'case' }]
639738
result = jbuild do |json|
640739
json.key_format! camelize: :lower
740+
json.deep_format_keys!
641741
json.array! names
642742
json.merge! new_names
643743
end
644744

645745
assert_equal %w[firstName lastName], result[1].keys
646746
end
647747

748+
test 'deep key_format! is applied to hash extracted from object' do
749+
comment = Struct.new(:author).new({ first_name: 'camel', last_name: 'case' })
750+
result = jbuild do |json|
751+
json.key_format! camelize: :lower
752+
json.deep_format_keys!
753+
json.extract! comment, :author
754+
end
755+
756+
assert_equal %w[firstName lastName], result['author'].keys
757+
end
758+
759+
test 'deep key_format! is applied to hash extracted from hash' do
760+
comment = {author: { first_name: 'camel', last_name: 'case' }}
761+
result = jbuild do |json|
762+
json.key_format! camelize: :lower
763+
json.deep_format_keys!
764+
json.extract! comment, :author
765+
end
766+
767+
assert_equal %w[firstName lastName], result['author'].keys
768+
end
769+
770+
test 'deep key_format! is applied to hash extracted directly from array' do
771+
comments = [Struct.new(:author).new({ first_name: 'camel', last_name: 'case' })]
772+
result = jbuild do |json|
773+
json.key_format! camelize: :lower
774+
json.deep_format_keys!
775+
json.array! comments, :author
776+
end
777+
778+
assert_equal %w[firstName lastName], result[0]['author'].keys
779+
end
780+
648781
test 'default key_format!' do
649782
Jbuilder.key_format camelize: :lower
650783
result = jbuild{ |json| json.camel_style 'for JS' }

0 commit comments

Comments
 (0)