Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
39 changes: 23 additions & 16 deletions lib/jbuilder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
require 'jbuilder/version'
require 'json'
require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/object/blank'

class Jbuilder
@@key_formatter = nil
Expand All @@ -33,14 +34,16 @@ def self.encode(*args, &block)
new(*args, &block).target!
end

BLANK = Blank.new
BLANK = Blank.new.freeze
EMPTY_ARRAY = [].freeze
private_constant :BLANK, :EMPTY_ARRAY

def set!(key, value = BLANK, *args, &block)
result = if ::Kernel.block_given?
if !_blank?(value)
# json.comments @post.comments { |comment| ... }
# { "comments": [ { ... }, { ... } ] }
_scope{ array! value, &block }
_scope{ _array value, &block }
Comment on lines -43 to +46
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.set! :posts, posts do |post|
  json.extract! post, :id, :body
end
ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
              before    74.247k i/100ms
               after    77.990k i/100ms
Calculating -------------------------------------
              before    755.086k (± 1.2%) i/s    (1.32 μs/i) -      3.787M in   5.015504s
               after    802.630k (± 1.4%) i/s    (1.25 μs/i) -      4.055M in   5.053783s

Comparison:
               after:   802630.0 i/s
              before:   755086.0 i/s - 1.06x  slower
Calculating -------------------------------------
              before   800.000  memsize (   520.000  retained)
                        11.000  objects (     4.000  retained)
                         0.000  strings (     0.000  retained)
               after   760.000  memsize (   520.000  retained)
                        10.000  objects (     4.000  retained)
                         0.000  strings (     0.000  retained)

Comparison:
               after:        760 allocated
              before:        800 allocated - 1.05x more

else
# json.comments { ... }
# { "comments": ... }
Expand All @@ -60,7 +63,7 @@ def set!(key, value = BLANK, *args, &block)
elsif _is_collection?(value)
# json.comments @post.comments, :content, :created_at
# { "comments": [ { "content": "hello", "created_at": "..." }, { "content": "world", "created_at": "..." } ] }
_scope{ array! value, *args }
_scope{ _array value, args }
Comment on lines -63 to +66
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

json.set! :posts, posts, :id, :body
ruby 3.4.4 (2025-05-14 revision a38531fd3f) +YJIT +PRISM [arm64-darwin24]
Warming up --------------------------------------
              before    69.118k i/100ms
               after    80.906k i/100ms
Calculating -------------------------------------
              before    718.268k (± 1.5%) i/s    (1.39 μs/i) -      3.594M in   5.005122s
               after    837.174k (± 1.5%) i/s    (1.19 μs/i) -      4.207M in   5.026501s

Comparison:
               after:   837173.8 i/s
              before:   718267.7 i/s - 1.17x  slower
Calculating -------------------------------------
              before   832.000  memsize (   520.000  retained)
                         8.000  objects (     4.000  retained)
                         0.000  strings (     0.000  retained)
               after   640.000  memsize (   520.000  retained)
                         7.000  objects (     4.000  retained)
                         0.000  strings (     0.000  retained)

Comparison:
               after:        640 allocated
              before:        832 allocated - 1.30x more

else
# json.author @post.creator, :name, :email_address
# { "author": { "name": "David", "email_address": "[email protected]" } }
Expand Down Expand Up @@ -207,18 +210,8 @@ def child!
# json.array! [1, 2, 3]
#
# [1,2,3]
def array!(collection = [], *attributes, &block)
array = if collection.nil?
[]
elsif ::Kernel.block_given?
_map_collection(collection, &block)
elsif attributes.any?
_map_collection(collection) { |element| _extract element, attributes }
else
_format_keys(collection.to_a)
end

@attributes = _merge_values(@attributes, array)
def array!(collection = EMPTY_ARRAY, *attributes, &block)
_array(collection, attributes, &block)
end

# Extracts the mentioned attributes or hash elements from the passed object and turns them into attributes of the JSON.
Expand All @@ -244,7 +237,7 @@ def extract!(object, *attributes)

def call(object, *attributes, &block)
if ::Kernel.block_given?
array! object, &block
_array object, &block
else
_extract object, attributes
end
Expand Down Expand Up @@ -277,6 +270,20 @@ def target!

alias_method :method_missing, :set!

def _array(collection = EMPTY_ARRAY, attributes = nil, &block)
array = if collection.nil?
EMPTY_ARRAY
elsif block
_map_collection(collection, &block)
elsif attributes.present?
_map_collection(collection) { |element| _extract element, attributes }
else
_format_keys(collection.to_a)
end

@attributes = _merge_values(@attributes, array)
end

def _extract(object, attributes)
if ::Hash === object
_extract_hash_values(object, attributes)
Expand Down
10 changes: 5 additions & 5 deletions lib/jbuilder/jbuilder_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,14 @@ def target!
@cached_root || super
end

def array!(collection = [], *args)
def array!(collection = EMPTY_ARRAY, *args, &block)
options = args.first

if args.one? && _partial_options?(options)
if _partial_options?(options)
options[:collection] = collection
_render_partial_with_options options
else
super
_array collection, args, &block
end
end

Expand Down Expand Up @@ -167,9 +167,9 @@ def _render_partial_with_options(options)
.new(@context.lookup_context, options) { |&block| _scope(&block) }
.render_collection_with_partial(collection, partial, @context, nil)

array! if results.respond_to?(:body) && results.body.nil?
_array if results.respond_to?(:body) && results.body.nil?
else
array!
_array
end
else
_render_partial options
Expand Down