Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve key formatting for nested hashes and disable by default #497

Merged
merged 2 commits into from
Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
49 changes: 39 additions & 10 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 @@ -190,10 +217,10 @@ def array!(collection = [], *attributes, &block)
elsif attributes.any?
_map_collection(collection) { |element| extract! element, *attributes }
else
collection.to_a
_format_keys(collection.to_a)
end

merge! array
@attributes = _merge_values(@attributes, array)
end

# Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
Expand Down Expand Up @@ -244,7 +271,7 @@ def attributes!
# Merges hash, array, or Jbuilder instance into current builder.
def merge!(object)
hash_or_array = ::Jbuilder === object ? object.attributes! : object
@attributes = _merge_values(@attributes, hash_or_array)
@attributes = _merge_values(@attributes, _format_keys(hash_or_array))
end

# Encodes the current builder as JSON.
Expand All @@ -255,11 +282,11 @@ def target!
private

def _extract_hash_values(object, attributes)
attributes.each{ |key| _set_value key, object.fetch(key) }
attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) }
end

def _extract_method_values(object, attributes)
attributes.each{ |key| _set_value key, object.public_send(key) }
attributes.each{ |key| _set_value key, _format_keys(object.public_send(key)) }
end

def _merge_block(key)
Expand All @@ -273,11 +300,11 @@ def _merge_values(current_value, updates)
if _blank?(updates)
current_value
elsif _blank?(current_value) || updates.nil? || current_value.empty? && ::Array === updates
_format_keys(updates)
updates
elsif ::Array === current_value && ::Array === updates
current_value + _format_keys(updates)
current_value + updates
elsif ::Hash === current_value && ::Hash === updates
current_value.deep_merge(_format_keys(updates))
current_value.deep_merge(updates)
else
raise MergeError.build(current_value, updates)
end
Expand All @@ -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
143 changes: 138 additions & 5 deletions test/jbuilder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,36 @@ class JbuilderTest < ActiveSupport::TestCase
assert_equal 'one', result['level1']
end

test 'key_format! can be changed in child elements' do
result = jbuild do |json|
json.key_format! camelize: :lower

json.level_one do
json.key_format! :upcase
json.value 'two'
end
end

assert_equal ['levelOne'], result.keys
assert_equal ['VALUE'], result['levelOne'].keys
end

test 'key_format! can be changed in array!' do
result = jbuild do |json|
json.key_format! camelize: :lower

json.level_one do
json.array! [{value: 'two'}] do |object|
json.key_format! :upcase
json.value object[:value]
end
end
end

assert_equal ['levelOne'], result.keys
assert_equal ['VALUE'], result['levelOne'][0].keys
end

test 'key_format! with no parameter' do
result = jbuild do |json|
json.key_format! :upcase
Expand Down Expand Up @@ -593,58 +623,161 @@ 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 array! of hashes' 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 '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 '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 '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 '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

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

test 'default key_format!' do
Jbuilder.key_format camelize: :lower
result = jbuild{ |json| json.camel_style 'for JS' }
Expand Down