Skip to content

Commit

Permalink
Disable deeply formatting keys of nested hashes by default
Browse files Browse the repository at this point in the history
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" }}
  • Loading branch information
tf committed Jan 27, 2021
1 parent b480e61 commit ade1b30
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 11 deletions.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,25 @@ environment.rb for example):
Jbuilder.key_format camelize: :lower
```

By default, key format is not applied to keys of hashes that are
passed to methods like `set!`, `array!` or `merge!`. You can opt into
deeply transforming these as well:

``` ruby
json.key_format! camelize: :lower
json.deep_format_keys!
json.settings([{some_value: "abc"}])

# => { "settings": [{ "someValue": "abc" }]}
```

You can set this globally with the class method `deep_format_keys` (from inside your
environment.rb for example):

``` ruby
Jbuilder.deep_format_keys true
```

## Contributing to Jbuilder

Jbuilder is the work of many contributors. You're encouraged to submit pull requests, propose
Expand Down
33 changes: 31 additions & 2 deletions lib/jbuilder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@
class Jbuilder
@@key_formatter = nil
@@ignore_nil = false
@@deep_format_keys = false

def initialize(options = {})
@attributes = {}

@key_formatter = options.fetch(:key_formatter){ @@key_formatter ? @@key_formatter.clone : nil}
@ignore_nil = options.fetch(:ignore_nil, @@ignore_nil)
@deep_format_keys = options.fetch(:deep_format_keys, @@deep_format_keys)

yield self if ::Kernel.block_given?
end
Expand Down Expand Up @@ -131,6 +133,31 @@ def self.ignore_nil(value = true)
@@ignore_nil = value
end

# Deeply apply key format to nested hashes and arrays passed to
# methods like set!, merge! or array!.
#
# Example:
#
# json.key_format! camelize: :lower
# json.settings({some_value: "abc"})
#
# { "settings": { "some_value": "abc" }}
#
# json.key_format! camelize: :lower
# json.deep_format_keys!
# json.settings({some_value: "abc"})
#
# { "settings": { "someValue": "abc" }}
#
def deep_format_keys!(value = true)
@deep_format_keys = value
end

# Same as instance method deep_format_keys! except sets the default.
def self.deep_format_keys(value = true)
@@deep_format_keys = value
end

# Turns the current element into an array and yields a builder to add a hash.
#
# Example:
Expand Down Expand Up @@ -288,6 +315,8 @@ def _key(key)
end

def _format_keys(hash_or_array)
return hash_or_array unless @deep_format_keys

if ::Array === hash_or_array
hash_or_array.map { |value| _format_keys(value) }
elsif ::Hash === hash_or_array
Expand All @@ -312,12 +341,12 @@ def _map_collection(collection)
end

def _scope
parent_attributes, parent_formatter = @attributes, @key_formatter
parent_attributes, parent_formatter, parent_deep_format_keys = @attributes, @key_formatter, @deep_format_keys
@attributes = BLANK
yield
@attributes
ensure
@attributes, @key_formatter = parent_attributes, parent_formatter
@attributes, @key_formatter, @deep_format_keys = parent_attributes, parent_formatter, parent_deep_format_keys
end

def _is_collection?(object)
Expand Down
81 changes: 72 additions & 9 deletions test/jbuilder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -623,92 +623,155 @@ class JbuilderTest < ActiveSupport::TestCase
assert_equal ['oats and friends'], result.keys
end

test 'key_format! with merge!' do
test 'key_format! is not applied deeply by default' do
names = { first_name: 'camel', last_name: 'case' }
result = jbuild do |json|
json.key_format! camelize: :lower
json.set! :all_names, names
end

assert_equal %i[first_name last_name], result['allNames'].keys
end

test 'applying key_format! deeply can be enabled per scope' do
names = { first_name: 'camel', last_name: 'case' }
result = jbuild do |json|
json.key_format! camelize: :lower
json.scope do
json.deep_format_keys!
json.set! :all_names, names
end
json.set! :all_names, names
end

assert_equal %w[firstName lastName], result['scope']['allNames'].keys
assert_equal %i[first_name last_name], result['allNames'].keys
end

test 'applying key_format! deeply can be disabled per scope' do
names = { first_name: 'camel', last_name: 'case' }
result = jbuild do |json|
json.key_format! camelize: :lower
json.deep_format_keys!
json.set! :all_names, names
json.scope do
json.deep_format_keys! false
json.set! :all_names, names
end
end

assert_equal %w[firstName lastName], result['allNames'].keys
assert_equal %i[first_name last_name], result['scope']['allNames'].keys
end

test 'applying key_format! deeply can be enabled globally' do
names = { first_name: 'camel', last_name: 'case' }

Jbuilder.deep_format_keys true
result = jbuild do |json|
json.key_format! camelize: :lower
json.set! :all_names, names
end

assert_equal %w[firstName lastName], result['allNames'].keys
Jbuilder.send(:class_variable_set, '@@deep_format_keys', false)
end

test 'deep key_format! with merge!' do
hash = { camel_style: 'for JS' }
result = jbuild do |json|
json.key_format! camelize: :lower
json.deep_format_keys!
json.merge! hash
end

assert_equal ['camelStyle'], result.keys
end

test 'key_format! with merge! deep' do
test 'deep key_format! with merge! deep' do
hash = { camel_style: { sub_attr: 'for JS' } }
result = jbuild do |json|
json.key_format! camelize: :lower
json.deep_format_keys!
json.merge! hash
end

assert_equal ['subAttr'], result['camelStyle'].keys
end

test 'key_format! with set! array of hashes' do
test 'deep key_format! with set! array of hashes' do
names = [{ first_name: 'camel', last_name: 'case' }]
result = jbuild do |json|
json.key_format! camelize: :lower
json.deep_format_keys!
json.set! :names, names
end

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

test 'key_format! with set! extracting hash from object' do
test 'deep key_format! with set! extracting hash from object' do
comment = Struct.new(:author).new({ first_name: 'camel', last_name: 'case' })
result = jbuild do |json|
json.key_format! camelize: :lower
json.deep_format_keys!
json.set! :comment, comment, :author
end

assert_equal %w[firstName lastName], result['comment']['author'].keys
end

test 'key_format! with array! of hashes' do
test 'deep key_format! with array! of hashes' do
names = [{ first_name: 'camel', last_name: 'case' }]
result = jbuild do |json|
json.key_format! camelize: :lower
json.deep_format_keys!
json.array! names
end

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

test 'key_format! with merge! array of hashes' do
test 'deep key_format! with merge! array of hashes' do
names = [{ first_name: 'camel', last_name: 'case' }]
new_names = [{ first_name: 'snake', last_name: 'case' }]
result = jbuild do |json|
json.key_format! camelize: :lower
json.deep_format_keys!
json.array! names
json.merge! new_names
end

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

test 'key_format! is applied to hash extracted from object' do
test 'deep key_format! is applied to hash extracted from object' do
comment = Struct.new(:author).new({ first_name: 'camel', last_name: 'case' })
result = jbuild do |json|
json.key_format! camelize: :lower
json.deep_format_keys!
json.extract! comment, :author
end

assert_equal %w[firstName lastName], result['author'].keys
end

test 'key_format! is applied to hash extracted from hash' do
test 'deep key_format! is applied to hash extracted from hash' do
comment = {author: { first_name: 'camel', last_name: 'case' }}
result = jbuild do |json|
json.key_format! camelize: :lower
json.deep_format_keys!
json.extract! comment, :author
end

assert_equal %w[firstName lastName], result['author'].keys
end

test 'key_format! is applied to hash extracted directly from array' do
test 'deep key_format! is applied to hash extracted directly from array' do
comments = [Struct.new(:author).new({ first_name: 'camel', last_name: 'case' })]
result = jbuild do |json|
json.key_format! camelize: :lower
json.deep_format_keys!
json.array! comments, :author
end

Expand Down

0 comments on commit ade1b30

Please sign in to comment.