From b9285506407ea9dd703112162b35fb7e08425bc4 Mon Sep 17 00:00:00 2001 From: Chris Oliver Date: Mon, 23 Sep 2024 13:26:41 -0500 Subject: [PATCH 01/43] Ensure cache helpers exists --- lib/jbuilder/railtie.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/jbuilder/railtie.rb b/lib/jbuilder/railtie.rb index 2aeefbb6..7ff1d6b7 100644 --- a/lib/jbuilder/railtie.rb +++ b/lib/jbuilder/railtie.rb @@ -20,6 +20,8 @@ module ApiRendering if name == 'ActionController::API' include ActionController::Helpers include ActionController::ImplicitRender + helper_method :combined_fragment_cache_key + helper_method :view_cache_dependencies end end end From 7e1f2efc0cb7c543ae107e28200246a14e3a4646 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Thu, 8 May 2025 13:50:13 -0400 Subject: [PATCH 02/43] Optimize _is_collection? method --- lib/jbuilder.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index b12624be..b2c75c5a 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -350,16 +350,12 @@ def _scope end def _is_collection?(object) - _object_respond_to?(object, :map, :count) && !(::Struct === object) + object.respond_to?(:map) && object.respond_to?(:count) && !(::Struct === object) end def _blank?(value=@attributes) BLANK == value end - - def _object_respond_to?(object, *methods) - methods.all?{ |m| object.respond_to?(m) } - end end require 'jbuilder/railtie' if defined?(Rails) From 86b394640a186f65f2ee194428e750d7d483d263 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Thu, 8 May 2025 15:29:43 -0400 Subject: [PATCH 03/43] Optimize options merging --- lib/jbuilder/jbuilder_template.rb | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 55f2d5ff..c929c6a6 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -4,11 +4,7 @@ require 'active_support/cache' class JbuilderTemplate < Jbuilder - class << self - attr_accessor :template_lookup_options - end - - self.template_lookup_options = { handlers: [:jbuilder] } + HANDLERS = [:jbuilder].freeze def initialize(context, *args) @context = context @@ -137,14 +133,14 @@ def set!(name, object = BLANK, *args) private def _render_partial_with_options(options) - options.reverse_merge! locals: options.except(:partial, :as, :collection, :cached) - options.reverse_merge! ::JbuilderTemplate.template_lookup_options + options[:locals] ||= options.except(:partial, :as, :collection, :cached) + options[:handlers] ||= ::JbuilderTemplate::HANDLERS as = options[:as] if as && options.key?(:collection) && CollectionRenderer.supported? collection = options.delete(:collection) || [] partial = options.delete(:partial) - options[:locals].merge!(json: self) + options[:locals][:json] = self collection = EnumerableCompat.new(collection) if collection.respond_to?(:count) && !collection.respond_to?(:size) if options.has_key?(:layout) @@ -173,8 +169,8 @@ def _render_partial_with_options(options) locals = options.delete(:locals) array! collection do |member| member_locals = locals.clone - member_locals.merge! collection: collection - member_locals.merge! as => member + member_locals[:collection] = collection + member_locals[as] = member _render_partial options.merge(locals: member_locals) end else @@ -186,7 +182,7 @@ def _render_partial_with_options(options) end def _render_partial(options) - options[:locals].merge! json: self + options[:locals][:json] = self @context.render options end From 866d2892c2b9fe4c407cb4437f954afd9637d8c3 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Fri, 9 May 2025 12:31:03 -0400 Subject: [PATCH 04/43] Call _set_value directly --- lib/jbuilder/jbuilder_template.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index c929c6a6..5228d5a3 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -244,7 +244,7 @@ def _set_inline_partial(name, object, options) _scope{ _render_partial_with_options options.merge(locals: locals) } end - set! name, value + _set_value name, value end def _render_explicit_partial(name_or_options, locals = {}) From c6b9bffed86c5a8dbdab0e475c92377b7eec1387 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 12 May 2025 11:48:07 -0400 Subject: [PATCH 05/43] Put template_lookup_options to avoid possible breaking change --- lib/jbuilder/jbuilder_template.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 5228d5a3..b4ba87e2 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -4,7 +4,11 @@ require 'active_support/cache' class JbuilderTemplate < Jbuilder - HANDLERS = [:jbuilder].freeze + class << self + attr_accessor :template_lookup_options + end + + self.template_lookup_options = { handlers: [:jbuilder] } def initialize(context, *args) @context = context @@ -134,7 +138,7 @@ def set!(name, object = BLANK, *args) def _render_partial_with_options(options) options[:locals] ||= options.except(:partial, :as, :collection, :cached) - options[:handlers] ||= ::JbuilderTemplate::HANDLERS + options[:handlers] ||= ::JbuilderTemplate.template_lookup_options[:handlers] as = options[:as] if as && options.key?(:collection) && CollectionRenderer.supported? From ba3a6274a9827773769f24a39956358059be7d11 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 12 May 2025 12:13:21 -0400 Subject: [PATCH 06/43] Save on more merges --- lib/jbuilder/jbuilder_template.rb | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index b4ba87e2..3dac0415 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -118,7 +118,8 @@ def array!(collection = [], *args) options = args.first if args.one? && _partial_options?(options) - partial! options.merge(collection: collection) + options[:collection] = collection + partial! options else super end @@ -175,7 +176,8 @@ def _render_partial_with_options(options) member_locals = locals.clone member_locals[:collection] = collection member_locals[as] = member - _render_partial options.merge(locals: member_locals) + options[:locals] = member_locals + _render_partial options end else array! @@ -242,10 +244,16 @@ def _set_inline_partial(name, object, options) value = if object.nil? [] elsif _is_collection?(object) - _scope{ _render_partial_with_options options.merge(collection: object) } + _scope do + options[:collection] = object + _render_partial_with_options options + end else locals = ::Hash[options[:as], object] - _scope{ _render_partial_with_options options.merge(locals: locals) } + _scope do + options[:locals] = locals + _render_partial_with_options options + end end _set_value name, value @@ -259,7 +267,8 @@ def _render_explicit_partial(name_or_options, locals = {}) else # partial! 'name', locals: {foo: 'bar'} if locals.one? && (locals.keys.first == :locals) - options = locals.merge(partial: name_or_options) + locals[:partial] = name_or_options + options = locals else options = { partial: name_or_options, locals: locals } end From 9ca4d0526996d1dfc6e97f1f9ccbc8d971581038 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 12 May 2025 16:50:23 -0400 Subject: [PATCH 07/43] Save memory allocation when calling render --- lib/jbuilder/jbuilder_template.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 3dac0415..74ed66e1 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -189,7 +189,7 @@ def _render_partial_with_options(options) def _render_partial(options) options[:locals][:json] = self - @context.render options + @context.render options, nil end def _cache_fragment_for(key, options, &block) From a9d12972b4345fdd6295583023494bbcbbdf61bf Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 28 May 2025 12:52:39 -0400 Subject: [PATCH 08/43] Prevent memory allocation on symbolized keys --- lib/jbuilder.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index b12624be..f501b64c 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -311,7 +311,13 @@ def _merge_values(current_value, updates) end def _key(key) - @key_formatter ? @key_formatter.format(key) : key.to_s + if @key_formatter + @key_formatter.format(key) + elsif key.is_a?(::Symbol) + key.name + else + key.to_s + end end def _format_keys(hash_or_array) From 776fa489dbcb3bae9e7eb9e82c9b668d010cc141 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 28 May 2025 16:31:08 -0400 Subject: [PATCH 09/43] Update gemspec for Rails 7 --- jbuilder.gemspec | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jbuilder.gemspec b/jbuilder.gemspec index 0b519531..60eec185 100644 --- a/jbuilder.gemspec +++ b/jbuilder.gemspec @@ -11,8 +11,8 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.2.2' - s.add_dependency 'activesupport', '>= 5.0.0' - s.add_dependency 'actionview', '>= 5.0.0' + s.add_dependency 'activesupport', '>= 7.0.0' + s.add_dependency 'actionview', '>= 7.0.0' if RUBY_ENGINE == 'rbx' s.add_development_dependency('racc') From 91f8664000fb71499356ac7dbca4031035c96627 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 28 May 2025 16:31:38 -0400 Subject: [PATCH 10/43] Update gemspec for Ruby 3.0 --- jbuilder.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jbuilder.gemspec b/jbuilder.gemspec index 60eec185..8a2580af 100644 --- a/jbuilder.gemspec +++ b/jbuilder.gemspec @@ -9,7 +9,7 @@ Gem::Specification.new do |s| s.homepage = 'https://github.com/rails/jbuilder' s.license = 'MIT' - s.required_ruby_version = '>= 2.2.2' + s.required_ruby_version = '>= 3.0.0' s.add_dependency 'activesupport', '>= 7.0.0' s.add_dependency 'actionview', '>= 7.0.0' From 84ac767d213aadb68590a873ea073deaf122a4d2 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 28 May 2025 16:33:35 -0400 Subject: [PATCH 11/43] Cleanup test cases for Rails 7 --- test/jbuilder_generator_test.rb | 14 +-- .../scaffold_api_controller_generator_test.rb | 112 ++++++++---------- test/scaffold_controller_generator_test.rb | 48 ++++---- 3 files changed, 79 insertions(+), 95 deletions(-) diff --git a/test/jbuilder_generator_test.rb b/test/jbuilder_generator_test.rb index e4a2f165..8b1ab9a8 100644 --- a/test/jbuilder_generator_test.rb +++ b/test/jbuilder_generator_test.rb @@ -56,15 +56,13 @@ class JbuilderGeneratorTest < Rails::Generators::TestCase end end - if Rails::VERSION::MAJOR >= 6 - test 'handles virtual attributes' do - run_generator %w(Message content:rich_text video:attachment photos:attachments) + test 'handles virtual attributes' do + run_generator %w(Message content:rich_text video:attachment photos:attachments) - assert_file 'app/views/messages/_message.json.jbuilder' do |content| - assert_match %r{json\.content message\.content\.to_s}, content - assert_match %r{json\.video url_for\(message\.video\)}, content - assert_match %r{json\.photos do\n json\.array!\(message\.photos\) do \|photo\|\n json\.id photo\.id\n json\.url url_for\(photo\)\n end\nend}, content - end + assert_file 'app/views/messages/_message.json.jbuilder' do |content| + assert_match %r{json\.content message\.content\.to_s}, content + assert_match %r{json\.video url_for\(message\.video\)}, content + assert_match %r{json\.photos do\n json\.array!\(message\.photos\) do \|photo\|\n json\.id photo\.id\n json\.url url_for\(photo\)\n end\nend}, content end end end diff --git a/test/scaffold_api_controller_generator_test.rb b/test/scaffold_api_controller_generator_test.rb index e7e2b355..8ab47082 100644 --- a/test/scaffold_api_controller_generator_test.rb +++ b/test/scaffold_api_controller_generator_test.rb @@ -2,81 +2,73 @@ require 'rails/generators/test_case' require 'generators/rails/scaffold_controller_generator' -if Rails::VERSION::MAJOR > 4 +class ScaffoldApiControllerGeneratorTest < Rails::Generators::TestCase + tests Rails::Generators::ScaffoldControllerGenerator + arguments %w(Post title body:text images:attachments --api) + destination File.expand_path('../tmp', __FILE__) + setup :prepare_destination - class ScaffoldApiControllerGeneratorTest < Rails::Generators::TestCase - tests Rails::Generators::ScaffoldControllerGenerator - arguments %w(Post title body:text images:attachments --api) - destination File.expand_path('../tmp', __FILE__) - setup :prepare_destination + test 'controller content' do + run_generator - test 'controller content' do - run_generator - - assert_file 'app/controllers/posts_controller.rb' do |content| - assert_instance_method :index, content do |m| - assert_match %r{@posts = Post\.all}, m - end + assert_file 'app/controllers/posts_controller.rb' do |content| + assert_instance_method :index, content do |m| + assert_match %r{@posts = Post\.all}, m + end - assert_instance_method :show, content do |m| - assert m.blank? - end + assert_instance_method :show, content do |m| + assert m.blank? + end - assert_instance_method :create, content do |m| - assert_match %r{@post = Post\.new\(post_params\)}, m - assert_match %r{@post\.save}, m - assert_match %r{render :show, status: :created, location: @post}, m - assert_match %r{render json: @post\.errors, status: :unprocessable_entity}, m - end + assert_instance_method :create, content do |m| + assert_match %r{@post = Post\.new\(post_params\)}, m + assert_match %r{@post\.save}, m + assert_match %r{render :show, status: :created, location: @post}, m + assert_match %r{render json: @post\.errors, status: :unprocessable_entity}, m + end - assert_instance_method :update, content do |m| - assert_match %r{render :show, status: :ok, location: @post}, m - assert_match %r{render json: @post.errors, status: :unprocessable_entity}, m - end + assert_instance_method :update, content do |m| + assert_match %r{render :show, status: :ok, location: @post}, m + assert_match %r{render json: @post.errors, status: :unprocessable_entity}, m + end - assert_instance_method :destroy, content do |m| - assert_match %r{@post\.destroy}, m - end + assert_instance_method :destroy, content do |m| + assert_match %r{@post\.destroy}, m + end - assert_match %r{def set_post}, content - if Rails::VERSION::MAJOR >= 8 - assert_match %r{params\.expect\(:id\)}, content - else - assert_match %r{params\[:id\]}, content - end + assert_match %r{def set_post}, content + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(:id\)}, content + else + assert_match %r{params\[:id\]}, content + end - assert_match %r{def post_params}, content - if Rails::VERSION::MAJOR >= 8 - assert_match %r{params\.expect\(post: \[ :title, :body, images: \[\] \]\)}, content - elsif Rails::VERSION::MAJOR >= 6 - assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content - else - assert_match %r{params\.require\(:post\)\.permit\(:title, :body, :images\)}, content - end + assert_match %r{def post_params}, content + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(post: \[ :title, :body, images: \[\] \]\)}, content + else + assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content end end + end - test "don't use require and permit if there are no attributes" do - run_generator %w(Post --api) + test "don't use require and permit if there are no attributes" do + run_generator %w(Post --api) - assert_file 'app/controllers/posts_controller.rb' do |content| - assert_match %r{def post_params}, content - assert_match %r{params\.fetch\(:post, \{\}\)}, content - end + assert_file 'app/controllers/posts_controller.rb' do |content| + assert_match %r{def post_params}, content + assert_match %r{params\.fetch\(:post, \{\}\)}, content end + end + test 'handles virtual attributes' do + run_generator ["Message", "content:rich_text", "video:attachment", "photos:attachments"] - if Rails::VERSION::MAJOR >= 6 - test 'handles virtual attributes' do - run_generator ["Message", "content:rich_text", "video:attachment", "photos:attachments"] - - assert_file 'app/controllers/messages_controller.rb' do |content| - if Rails::VERSION::MAJOR >= 8 - assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content - else - assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content - end - end + assert_file 'app/controllers/messages_controller.rb' do |content| + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content + else + assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content end end end diff --git a/test/scaffold_controller_generator_test.rb b/test/scaffold_controller_generator_test.rb index a642eef9..795dec8e 100644 --- a/test/scaffold_controller_generator_test.rb +++ b/test/scaffold_controller_generator_test.rb @@ -60,29 +60,25 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase assert_match %r{def post_params}, content if Rails::VERSION::MAJOR >= 8 assert_match %r{params\.expect\(post: \[ :title, :body, images: \[\] \]\)}, content - elsif Rails::VERSION::MAJOR >= 6 - assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content else - assert_match %r{params\.require\(:post\)\.permit\(:title, :body, :images\)}, content + assert_match %r{params\.require\(:post\)\.permit\(:title, :body, images: \[\]\)}, content end end end - if Rails::VERSION::MAJOR >= 6 - test 'controller with namespace' do - run_generator %w(Admin::Post --model-name=Post) - assert_file 'app/controllers/admin/posts_controller.rb' do |content| - assert_instance_method :create, content do |m| - assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully created\." \}}, m - end - - assert_instance_method :update, content do |m| - assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully updated\.", status: :see_other \}}, m - end - - assert_instance_method :destroy, content do |m| - assert_match %r{format\.html \{ redirect_to admin_posts_path, notice: "Post was successfully destroyed\.", status: :see_other \}}, m - end + test 'controller with namespace' do + run_generator %w(Admin::Post --model-name=Post) + assert_file 'app/controllers/admin/posts_controller.rb' do |content| + assert_instance_method :create, content do |m| + assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully created\." \}}, m + end + + assert_instance_method :update, content do |m| + assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully updated\.", status: :see_other \}}, m + end + + assert_instance_method :destroy, content do |m| + assert_match %r{format\.html \{ redirect_to admin_posts_path, notice: "Post was successfully destroyed\.", status: :see_other \}}, m end end end @@ -96,16 +92,14 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase end end - if Rails::VERSION::MAJOR >= 6 - test 'handles virtual attributes' do - run_generator %w(Message content:rich_text video:attachment photos:attachments) + test 'handles virtual attributes' do + run_generator %w(Message content:rich_text video:attachment photos:attachments) - assert_file 'app/controllers/messages_controller.rb' do |content| - if Rails::VERSION::MAJOR >= 8 - assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content - else - assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content - end + assert_file 'app/controllers/messages_controller.rb' do |content| + if Rails::VERSION::MAJOR >= 8 + assert_match %r{params\.expect\(message: \[ :content, :video, photos: \[\] \]\)}, content + else + assert_match %r{params\.require\(:message\)\.permit\(:content, :video, photos: \[\]\)}, content end end end From a4dc35ef73b655e0af6f97bef2bdd61b0cde5d37 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 28 May 2025 16:34:08 -0400 Subject: [PATCH 12/43] Cleanup Railtie for Rails 7 --- lib/jbuilder/railtie.rb | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/lib/jbuilder/railtie.rb b/lib/jbuilder/railtie.rb index 2aeefbb6..490ad2f0 100644 --- a/lib/jbuilder/railtie.rb +++ b/lib/jbuilder/railtie.rb @@ -9,28 +9,24 @@ class Railtie < ::Rails::Railtie require 'jbuilder/jbuilder_dependency_tracker' end - if Rails::VERSION::MAJOR >= 5 - module ::ActionController - module ApiRendering - include ActionView::Rendering - end + module ::ActionController + module ApiRendering + include ActionView::Rendering end + end - ActiveSupport.on_load :action_controller do - if name == 'ActionController::API' - include ActionController::Helpers - include ActionController::ImplicitRender - end + ActiveSupport.on_load :action_controller do + if name == 'ActionController::API' + include ActionController::Helpers + include ActionController::ImplicitRender end end end - if Rails::VERSION::MAJOR >= 4 - generators do |app| - Rails::Generators.configure! app.config.generators - Rails::Generators.hidden_namespaces.uniq! - require 'generators/rails/scaffold_controller_generator' - end + generators do |app| + Rails::Generators.configure! app.config.generators + Rails::Generators.hidden_namespaces.uniq! + require 'generators/rails/scaffold_controller_generator' end end end From c6aebce831eaa45546e894f7c2f6142a9605505e Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 28 May 2025 16:36:33 -0400 Subject: [PATCH 13/43] Cleanup test cases for Ruby 3.0 --- test/jbuilder_test.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/test/jbuilder_test.rb b/test/jbuilder_test.rb index 76569bb6..29a4d6ba 100644 --- a/test/jbuilder_test.rb +++ b/test/jbuilder_test.rb @@ -930,12 +930,10 @@ class JbuilderTest < ActiveSupport::TestCase end end - if RUBY_VERSION >= "2.2.10" - test "respects JSON encoding customizations" do - # Active Support overrides Time#as_json for custom formatting. - # Ensure we call #to_json on the final attributes instead of JSON.dump. - result = JSON.load(Jbuilder.encode { |json| json.time Time.parse("2018-05-13 11:51:00.485 -0400") }) - assert_equal "2018-05-13T11:51:00.485-04:00", result["time"] - end + test "respects JSON encoding customizations" do + # Active Support overrides Time#as_json for custom formatting. + # Ensure we call #to_json on the final attributes instead of JSON.dump. + result = JSON.load(Jbuilder.encode { |json| json.time Time.parse("2018-05-13 11:51:00.485 -0400") }) + assert_equal "2018-05-13T11:51:00.485-04:00", result["time"] end end From e46bf9a21b92bfd2f7fb47bacac467a75c92a209 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 28 May 2025 16:46:53 -0400 Subject: [PATCH 14/43] Refactor CollectionRenderer for Rails 7+ support --- lib/jbuilder/collection_renderer.rb | 94 ++++-------------- lib/jbuilder/jbuilder_template.rb | 18 +--- test/jbuilder_template_test.rb | 142 ++++++++++++++-------------- 3 files changed, 88 insertions(+), 166 deletions(-) diff --git a/lib/jbuilder/collection_renderer.rb b/lib/jbuilder/collection_renderer.rb index 48707bfe..94b2fdf1 100644 --- a/lib/jbuilder/collection_renderer.rb +++ b/lib/jbuilder/collection_renderer.rb @@ -1,37 +1,9 @@ require 'delegate' -require 'active_support/concern' require 'action_view' - -begin - require 'action_view/renderer/collection_renderer' -rescue LoadError - require 'action_view/renderer/partial_renderer' -end +require 'action_view/renderer/collection_renderer' class Jbuilder - module CollectionRenderable # :nodoc: - extend ActiveSupport::Concern - - class_methods do - def supported? - superclass.private_method_defined?(:build_rendered_template) && self.superclass.private_method_defined?(:build_rendered_collection) - end - end - - private - - def build_rendered_template(content, template, layout = nil) - super(content || json.attributes!, template) - end - - def build_rendered_collection(templates, _spacer) - json.merge!(templates.map(&:body)) - end - - def json - @options[:locals].fetch(:json) - end - + class CollectionRenderer < ::ActionView::CollectionRenderer # :nodoc: class ScopedIterator < ::SimpleDelegator # :nodoc: include Enumerable @@ -40,16 +12,6 @@ def initialize(obj, scope) @scope = scope end - # Rails 6.0 support: - def each - return enum_for(:each) unless block_given? - - __getobj__.each do |object| - @scope.call { yield(object) } - end - end - - # Rails 6.1 support: def each_with_info return enum_for(:each_with_info) unless block_given? @@ -60,51 +22,29 @@ def each_with_info end private_constant :ScopedIterator - end - - if defined?(::ActionView::CollectionRenderer) - # Rails 6.1 support: - class CollectionRenderer < ::ActionView::CollectionRenderer # :nodoc: - include CollectionRenderable - def initialize(lookup_context, options, &scope) - super(lookup_context, options) - @scope = scope - end - - private - def collection_with_template(view, template, layout, collection) - super(view, template, layout, ScopedIterator.new(collection, @scope)) - end + def initialize(lookup_context, options, &scope) + super(lookup_context, options) + @scope = scope end - else - # Rails 6.0 support: - class CollectionRenderer < ::ActionView::PartialRenderer # :nodoc: - include CollectionRenderable - def initialize(lookup_context, options, &scope) - super(lookup_context) - @options = options - @scope = scope - end + private - def render_collection_with_partial(collection, partial, context, block) - render(context, @options.merge(collection: collection, partial: partial), block) + def build_rendered_template(content, template, layout = nil) + super(content || json.attributes!, template) end - private - def collection_without_template(view) - @collection = ScopedIterator.new(@collection, @scope) - - super(view) - end + def build_rendered_collection(templates, _spacer) + json.merge!(templates.map(&:body)) + end - def collection_with_template(view, template) - @collection = ScopedIterator.new(@collection, @scope) + def json + @options[:locals].fetch(:json) + end - super(view, template) - end - end + def collection_with_template(view, template, layout, collection) + super(view, template, layout, ScopedIterator.new(collection, @scope)) + end end class EnumerableCompat < ::SimpleDelegator diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 55f2d5ff..1326c9d6 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -141,7 +141,7 @@ def _render_partial_with_options(options) options.reverse_merge! ::JbuilderTemplate.template_lookup_options as = options[:as] - if as && options.key?(:collection) && CollectionRenderer.supported? + if as && options.key?(:collection) collection = options.delete(:collection) || [] partial = options.delete(:partial) options[:locals].merge!(json: self) @@ -164,22 +164,6 @@ def _render_partial_with_options(options) else array! end - elsif as && options.key?(:collection) && !CollectionRenderer.supported? - # For Rails <= 5.2: - as = as.to_sym - collection = options.delete(:collection) - - if collection.present? - locals = options.delete(:locals) - array! collection do |member| - member_locals = locals.clone - member_locals.merge! collection: collection - member_locals.merge! as => member - _render_partial options.merge(locals: member_locals) - end - else - array! - end else _render_partial options end diff --git a/test/jbuilder_template_test.rb b/test/jbuilder_template_test.rb index 54addb23..66c5e314 100644 --- a/test/jbuilder_template_test.rb +++ b/test/jbuilder_template_test.rb @@ -317,99 +317,97 @@ class JbuilderTemplateTest < ActiveSupport::TestCase assert_equal "David", result["firstName"] end - if JbuilderTemplate::CollectionRenderer.supported? - test "returns an empty array for an empty collection" do - Jbuilder::CollectionRenderer.expects(:new).never - result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: []) + test "returns an empty array for an empty collection" do + Jbuilder::CollectionRenderer.expects(:new).never + result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: []) - # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array. - assert_equal [], result - end + # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array. + assert_equal [], result + end - test "works with an enumerable object" do - enumerable_class = Class.new do - include Enumerable + test "works with an enumerable object" do + enumerable_class = Class.new do + include Enumerable - def each(&block) - [].each(&block) - end + def each(&block) + [].each(&block) end + end - result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: enumerable_class.new) + result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: enumerable_class.new) - # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array. - assert_equal [], result - end + # Do not use #assert_empty as it is important to ensure that the type of the JSON result is an array. + assert_equal [], result + end + + test "supports the cached: true option" do + result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: POSTS) + + assert_equal 10, result.count + assert_equal "Post #5", result[4]["body"] + assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] + assert_equal "Pavel", result[5]["author"]["first_name"] - test "supports the cached: true option" do - result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: POSTS) - - assert_equal 10, result.count - assert_equal "Post #5", result[4]["body"] - assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] - assert_equal "Pavel", result[5]["author"]["first_name"] - - expected = { - "id" => 1, - "body" => "Post #1", - "author" => { - "first_name" => "David", - "last_name" => "Heinemeier Hansson" - } + expected = { + "id" => 1, + "body" => "Post #1", + "author" => { + "first_name" => "David", + "last_name" => "Heinemeier Hansson" } + } - assert_equal expected, Rails.cache.read("post-1") + assert_equal expected, Rails.cache.read("post-1") - result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: POSTS) + result = render('json.array! @posts, partial: "post", as: :post, cached: true', posts: POSTS) - assert_equal 10, result.count - assert_equal "Post #5", result[4]["body"] - assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] - assert_equal "Pavel", result[5]["author"]["first_name"] - end + assert_equal 10, result.count + assert_equal "Post #5", result[4]["body"] + assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] + assert_equal "Pavel", result[5]["author"]["first_name"] + end - test "supports the cached: ->() {} option" do - result = render('json.array! @posts, partial: "post", as: :post, cached: ->(post) { [post, "foo"] }', posts: POSTS) - - assert_equal 10, result.count - assert_equal "Post #5", result[4]["body"] - assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] - assert_equal "Pavel", result[5]["author"]["first_name"] - - expected = { - "id" => 1, - "body" => "Post #1", - "author" => { - "first_name" => "David", - "last_name" => "Heinemeier Hansson" - } - } + test "supports the cached: ->() {} option" do + result = render('json.array! @posts, partial: "post", as: :post, cached: ->(post) { [post, "foo"] }', posts: POSTS) - assert_equal expected, Rails.cache.read("post-1/foo") + assert_equal 10, result.count + assert_equal "Post #5", result[4]["body"] + assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] + assert_equal "Pavel", result[5]["author"]["first_name"] - result = render('json.array! @posts, partial: "post", as: :post, cached: ->(post) { [post, "foo"] }', posts: POSTS) + expected = { + "id" => 1, + "body" => "Post #1", + "author" => { + "first_name" => "David", + "last_name" => "Heinemeier Hansson" + } + } - assert_equal 10, result.count - assert_equal "Post #5", result[4]["body"] - assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] - assert_equal "Pavel", result[5]["author"]["first_name"] - end + assert_equal expected, Rails.cache.read("post-1/foo") - test "raises an error on a render call with the :layout option" do - error = assert_raises NotImplementedError do - render('json.array! @posts, partial: "post", as: :post, layout: "layout"', posts: POSTS) - end + result = render('json.array! @posts, partial: "post", as: :post, cached: ->(post) { [post, "foo"] }', posts: POSTS) - assert_equal "The `:layout' option is not supported in collection rendering.", error.message + assert_equal 10, result.count + assert_equal "Post #5", result[4]["body"] + assert_equal "Heinemeier Hansson", result[2]["author"]["last_name"] + assert_equal "Pavel", result[5]["author"]["first_name"] + end + + test "raises an error on a render call with the :layout option" do + error = assert_raises NotImplementedError do + render('json.array! @posts, partial: "post", as: :post, layout: "layout"', posts: POSTS) end - test "raises an error on a render call with the :spacer_template option" do - error = assert_raises NotImplementedError do - render('json.array! @posts, partial: "post", as: :post, spacer_template: "template"', posts: POSTS) - end + assert_equal "The `:layout' option is not supported in collection rendering.", error.message + end - assert_equal "The `:spacer_template' option is not supported in collection rendering.", error.message + test "raises an error on a render call with the :spacer_template option" do + error = assert_raises NotImplementedError do + render('json.array! @posts, partial: "post", as: :post, spacer_template: "template"', posts: POSTS) end + + assert_equal "The `:spacer_template' option is not supported in collection rendering.", error.message end private From 0409614e99a7f79bcf9ebde98d342c1d18645216 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Thu, 29 May 2025 16:19:41 -0400 Subject: [PATCH 15/43] Cleanup one more spot in tests --- test/jbuilder_template_test.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/test/jbuilder_template_test.rb b/test/jbuilder_template_test.rb index 66c5e314..76368e12 100644 --- a/test/jbuilder_template_test.rb +++ b/test/jbuilder_template_test.rb @@ -425,12 +425,7 @@ def build_view(options = {}) lookup_context = ActionView::LookupContext.new([ resolver ], {}, [""]) controller = ActionView::TestCase::TestController.new - # TODO: Use with_empty_template_cache unconditionally after dropping support for Rails <6.0. - view = if ActionView::Base.respond_to?(:with_empty_template_cache) - ActionView::Base.with_empty_template_cache.new(lookup_context, options.fetch(:assigns, {}), controller) - else - ActionView::Base.new(lookup_context, options.fetch(:assigns, {}), controller) - end + view = ActionView::Base.with_empty_template_cache.new(lookup_context, options.fetch(:assigns, {}), controller) def view.view_cache_dependencies; []; end def view.combined_fragment_cache_key(key) [ key ] end From 7c58bcc5dcddec8695e938a35e4c4c581c08f537 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Fri, 30 May 2025 10:11:05 -0400 Subject: [PATCH 16/43] Optimize key formatter --- lib/jbuilder/key_formatter.rb | 36 ++++++++++++++--------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/lib/jbuilder/key_formatter.rb b/lib/jbuilder/key_formatter.rb index c9e7eca3..5d4a6e76 100644 --- a/lib/jbuilder/key_formatter.rb +++ b/lib/jbuilder/key_formatter.rb @@ -1,34 +1,26 @@ require 'jbuilder/jbuilder' -require 'active_support/core_ext/array' class Jbuilder class KeyFormatter - def initialize(*args) - @format = {} - @cache = {} + def initialize(*formats, **formats_with_options) + @cache = + Hash.new do |hash, key| + value = key.is_a?(Symbol) ? key.name : key.to_s - options = args.extract_options! - args.each do |name| - @format[name] = [] - end - options.each do |name, parameters| - @format[name] = parameters - end - end + formats.each do |func| + value = func.is_a?(Proc) ? func.call(value) : value.send(func) + end + + formats_with_options.each do |func, params| + value = func.is_a?(Proc) ? func.call(value, *params) : value.send(func, *params) + end - def initialize_copy(original) - @cache = {} + hash[key] = value + end end def format(key) - @cache[key] ||= @format.inject(key.to_s) do |result, args| - func, args = args - if ::Proc === func - func.call result, *args - else - result.send func, *args - end - end + @cache[key] end end end From b524e6c5a28af10302cc278cf7a1ef7e06038db1 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Fri, 30 May 2025 10:11:45 -0400 Subject: [PATCH 17/43] Reuse key cache across template renders --- lib/jbuilder.rb | 6 +++--- test/jbuilder_test.rb | 8 -------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index b12624be..4d183376 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -15,9 +15,9 @@ class Jbuilder 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) + @key_formatter = options&.[](:key_formatter) || @@key_formatter + @ignore_nil = options&.[](:ignore_nil) || @@ignore_nil + @deep_format_keys = options&.[](:deep_format_keys) || @@deep_format_keys yield self if ::Kernel.block_given? end diff --git a/test/jbuilder_test.rb b/test/jbuilder_test.rb index 76569bb6..34156c30 100644 --- a/test/jbuilder_test.rb +++ b/test/jbuilder_test.rb @@ -784,14 +784,6 @@ class JbuilderTest < ActiveSupport::TestCase assert_equal ['camelStyle'], result.keys end - test 'do not use default key formatter directly' do - Jbuilder.key_format - jbuild{ |json| json.key 'value' } - formatter = Jbuilder.send(:class_variable_get, '@@key_formatter') - cache = formatter.instance_variable_get('@cache') - assert_empty cache - end - test 'ignore_nil! without a parameter' do result = jbuild do |json| json.ignore_nil! From 47824afc270b3879635eade5243e497860aff866 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Fri, 30 May 2025 10:12:02 -0400 Subject: [PATCH 18/43] Save on some memory allocation during initialization --- lib/jbuilder.rb | 10 +++++----- lib/jbuilder/jbuilder_template.rb | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index 4d183376..5b226862 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -12,7 +12,7 @@ class Jbuilder @@ignore_nil = false @@deep_format_keys = false - def initialize(options = {}) + def initialize(options = nil) @attributes = {} @key_formatter = options&.[](:key_formatter) || @@key_formatter @@ -100,13 +100,13 @@ def method_missing(*args, &block) # # { "_first_name": "David" } # - def key_format!(*args) - @key_formatter = KeyFormatter.new(*args) + def key_format!(...) + @key_formatter = KeyFormatter.new(...) end # Same as the instance method key_format! except sets the default. - def self.key_format(*args) - @@key_formatter = KeyFormatter.new(*args) + def self.key_format(...) + @@key_formatter = KeyFormatter.new(...) end # If you want to skip adding nil values to your JSON hash. This is useful diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 55f2d5ff..ec4d7b57 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -10,10 +10,10 @@ class << self self.template_lookup_options = { handlers: [:jbuilder] } - def initialize(context, *args) + def initialize(context, options = nil) @context = context @cached_root = nil - super(*args) + super(options) end # Generates JSON using the template specified with the `:partial` option. For example, the code below will render From 25f48f49aa4cc35f5877ae5b434a81f00ca7353c Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Fri, 30 May 2025 13:01:44 -0400 Subject: [PATCH 19/43] Optimize extract to save on memory allocation --- lib/jbuilder.rb | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index b12624be..e757faa9 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -58,7 +58,7 @@ def set!(key, value = BLANK, *args, &block) else # json.author @post.creator, :name, :email_address # { "author": { "name": "David", "email_address": "david@loudthinking.com" } } - _merge_block(key){ extract! value, *args } + _merge_block(key){ _extract value, args } end _set_value key, result @@ -215,7 +215,7 @@ def array!(collection = [], *attributes, &block) elsif ::Kernel.block_given? _map_collection(collection, &block) elsif attributes.any? - _map_collection(collection) { |element| extract! element, *attributes } + _map_collection(collection) { |element| _extract element, attributes } else _format_keys(collection.to_a) end @@ -241,18 +241,14 @@ def array!(collection = [], *attributes, &block) # # json.(@person, :name, :age) def extract!(object, *attributes) - if ::Hash === object - _extract_hash_values(object, attributes) - else - _extract_method_values(object, attributes) - end + _extract object, attributes end def call(object, *attributes, &block) if ::Kernel.block_given? array! object, &block else - extract! object, *attributes + _extract object, attributes end end @@ -281,8 +277,16 @@ def target! private + def _extract(object, attributes) + if ::Hash === object + _extract_hash_values(object, attributes) + else + _extract_method_values(object, attributes) + end + end + def _extract_hash_values(object, attributes) - attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) } + attributes.each{ |key| _set_value key, _format_keys(object[key]) } end def _extract_method_values(object, attributes) From 06224642c48f1d521139b9a348336fbe02887844 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 2 Jun 2025 11:13:59 -0400 Subject: [PATCH 20/43] Back to fetch --- lib/jbuilder.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index e757faa9..fd91852b 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -286,7 +286,7 @@ def _extract(object, attributes) end def _extract_hash_values(object, attributes) - attributes.each{ |key| _set_value key, _format_keys(object[key]) } + attributes.each{ |key| _set_value key, _format_keys(object.fetch(key)) } end def _extract_method_values(object, attributes) From a6f1eedc435040e94b11230ab405a3b8ee1f5f1f Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 2 Jun 2025 16:05:30 -0400 Subject: [PATCH 21/43] Use mutex for thread safety --- lib/jbuilder/key_formatter.rb | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/jbuilder/key_formatter.rb b/lib/jbuilder/key_formatter.rb index 5d4a6e76..2c0f1f99 100644 --- a/lib/jbuilder/key_formatter.rb +++ b/lib/jbuilder/key_formatter.rb @@ -3,24 +3,28 @@ class Jbuilder class KeyFormatter def initialize(*formats, **formats_with_options) - @cache = - Hash.new do |hash, key| + @mutex = Mutex.new + @formats = formats + @formats_with_options = formats_with_options + @cache = {} + end + + def format(key) + @mutex.synchronize do + @cache[key] ||= begin value = key.is_a?(Symbol) ? key.name : key.to_s - formats.each do |func| + @formats.each do |func| value = func.is_a?(Proc) ? func.call(value) : value.send(func) end - formats_with_options.each do |func, params| + @formats_with_options.each do |func, params| value = func.is_a?(Proc) ? func.call(value, *params) : value.send(func, *params) end - hash[key] = value + value end - end - - def format(key) - @cache[key] + end end end end From 964ef0b794106bd00525dd7a408817f0dbf29902 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 2 Jun 2025 16:47:52 -0400 Subject: [PATCH 22/43] Optimize jbuilder initialization --- lib/jbuilder.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index 5b226862..382ad3d5 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -15,9 +15,15 @@ class Jbuilder def initialize(options = nil) @attributes = {} - @key_formatter = options&.[](:key_formatter) || @@key_formatter - @ignore_nil = options&.[](:ignore_nil) || @@ignore_nil - @deep_format_keys = options&.[](:deep_format_keys) || @@deep_format_keys + if options + @key_formatter = options[:key_formatter] + @ignore_nil = options[:ignore_nil] + @deep_format_keys = options[:deep_format_keys] + else + @key_formatter = @@key_formatter + @ignore_nil = @@ignore_nil + @deep_format_keys = @@deep_format_keys + end yield self if ::Kernel.block_given? end From 34814e78bda685b0ef9f4a69b6208dd71e263cf7 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Tue, 3 Jun 2025 10:34:23 -0400 Subject: [PATCH 23/43] Repurpose test to validate default formatter cache is used --- test/jbuilder_test.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/jbuilder_test.rb b/test/jbuilder_test.rb index 34156c30..4f2cbe20 100644 --- a/test/jbuilder_test.rb +++ b/test/jbuilder_test.rb @@ -784,6 +784,14 @@ class JbuilderTest < ActiveSupport::TestCase assert_equal ['camelStyle'], result.keys end + test 'use default key formatter when configured' do + Jbuilder.key_format + jbuild{ |json| json.key 'value' } + formatter = Jbuilder.send(:class_variable_get, '@@key_formatter') + cache = formatter.instance_variable_get('@cache') + assert_includes cache, :key + end + test 'ignore_nil! without a parameter' do result = jbuild do |json| json.ignore_nil! From 5ea899be843106822662bdc3ffbf8771ac7ae5be Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Tue, 3 Jun 2025 10:37:44 -0400 Subject: [PATCH 24/43] Invert options nil check --- lib/jbuilder.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index 382ad3d5..cc3f5f05 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -15,14 +15,14 @@ class Jbuilder def initialize(options = nil) @attributes = {} - if options - @key_formatter = options[:key_formatter] - @ignore_nil = options[:ignore_nil] - @deep_format_keys = options[:deep_format_keys] - else + if options.nil? @key_formatter = @@key_formatter @ignore_nil = @@ignore_nil @deep_format_keys = @@deep_format_keys + else + @key_formatter = options[:key_formatter] + @ignore_nil = options[:ignore_nil] + @deep_format_keys = options[:deep_format_keys] end yield self if ::Kernel.block_given? From be609e7b10bac8d5c3351fd6d97afcb9560e5133 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Tue, 3 Jun 2025 10:43:18 -0400 Subject: [PATCH 25/43] Cleanup initializer --- lib/jbuilder.rb | 15 ++++----------- lib/jbuilder/jbuilder_template.rb | 3 ++- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index cc3f5f05..fad1962c 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -12,18 +12,11 @@ class Jbuilder @@ignore_nil = false @@deep_format_keys = false - def initialize(options = nil) + def initialize(key_formatter: @@key_formatter, ignore_nil: @@ignore_nil, deep_format_keys: @@deep_format_keys) @attributes = {} - - if options.nil? - @key_formatter = @@key_formatter - @ignore_nil = @@ignore_nil - @deep_format_keys = @@deep_format_keys - else - @key_formatter = options[:key_formatter] - @ignore_nil = options[:ignore_nil] - @deep_format_keys = options[:deep_format_keys] - end + @key_formatter = key_formatter + @ignore_nil = ignore_nil + @deep_format_keys = deep_format_keys yield self if ::Kernel.block_given? end diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index ec4d7b57..2984aafd 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -13,7 +13,8 @@ class << self def initialize(context, options = nil) @context = context @cached_root = nil - super(options) + + options.nil? ? super() : super(**options) end # Generates JSON using the template specified with the `:partial` option. For example, the code below will render From e0d60fdbd6dd621b523e9a1eaeea1470a84eb382 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 4 Jun 2025 10:35:28 -0400 Subject: [PATCH 26/43] Save on call to ::Kernal.block_given? --- lib/jbuilder.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index fad1962c..db2f25c8 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -12,13 +12,18 @@ class Jbuilder @@ignore_nil = false @@deep_format_keys = false - def initialize(key_formatter: @@key_formatter, ignore_nil: @@ignore_nil, deep_format_keys: @@deep_format_keys) + def initialize( + key_formatter: @@key_formatter, + ignore_nil: @@ignore_nil, + deep_format_keys: @@deep_format_keys, + &block + ) @attributes = {} @key_formatter = key_formatter @ignore_nil = ignore_nil @deep_format_keys = deep_format_keys - yield self if ::Kernel.block_given? + yield self if block end # Yields a builder and automatically turns the result into a JSON string From a929dd64b7a95da3042561e20ef3ab85a70905b8 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Thu, 5 Jun 2025 13:38:31 -0400 Subject: [PATCH 27/43] Add frozen_string_literal --- Appraisals | 2 ++ Gemfile | 2 ++ Rakefile | 2 ++ jbuilder.gemspec | 2 ++ lib/generators/rails/jbuilder_generator.rb | 2 ++ lib/generators/rails/scaffold_controller_generator.rb | 2 ++ lib/jbuilder.rb | 2 ++ lib/jbuilder/blank.rb | 2 ++ lib/jbuilder/collection_renderer.rb | 2 ++ lib/jbuilder/errors.rb | 2 ++ lib/jbuilder/jbuilder.rb | 2 ++ lib/jbuilder/jbuilder_dependency_tracker.rb | 2 ++ lib/jbuilder/jbuilder_template.rb | 2 ++ lib/jbuilder/key_formatter.rb | 2 ++ lib/jbuilder/railtie.rb | 2 ++ lib/jbuilder/version.rb | 2 ++ 16 files changed, 32 insertions(+) diff --git a/Appraisals b/Appraisals index 516847c7..3d1832d2 100644 --- a/Appraisals +++ b/Appraisals @@ -1,3 +1,5 @@ +# frozen_string_literal: true + appraise "rails-7-0" do gem "rails", "~> 7.0.0" gem "concurrent-ruby", "< 1.3.5" # to avoid problem described in https://github.com/rails/rails/pull/54264 diff --git a/Gemfile b/Gemfile index 77021588..02c3b379 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + source "https://rubygems.org" gemspec diff --git a/Rakefile b/Rakefile index 3a28e6fa..4b82612e 100644 --- a/Rakefile +++ b/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "bundler/setup" require "bundler/gem_tasks" require "rake/testtask" diff --git a/jbuilder.gemspec b/jbuilder.gemspec index 0b519531..2360467d 100644 --- a/jbuilder.gemspec +++ b/jbuilder.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require_relative "lib/jbuilder/version" Gem::Specification.new do |s| diff --git a/lib/generators/rails/jbuilder_generator.rb b/lib/generators/rails/jbuilder_generator.rb index 79f742e8..81eb54e7 100644 --- a/lib/generators/rails/jbuilder_generator.rb +++ b/lib/generators/rails/jbuilder_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails/generators/named_base' require 'rails/generators/resource_helpers' diff --git a/lib/generators/rails/scaffold_controller_generator.rb b/lib/generators/rails/scaffold_controller_generator.rb index bf48f0cb..42407c09 100644 --- a/lib/generators/rails/scaffold_controller_generator.rb +++ b/lib/generators/rails/scaffold_controller_generator.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails/generators' require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index b12624be..466f9c11 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_support' require 'jbuilder/jbuilder' require 'jbuilder/blank' diff --git a/lib/jbuilder/blank.rb b/lib/jbuilder/blank.rb index 52af2c6f..41169135 100644 --- a/lib/jbuilder/blank.rb +++ b/lib/jbuilder/blank.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Jbuilder class Blank def ==(other) diff --git a/lib/jbuilder/collection_renderer.rb b/lib/jbuilder/collection_renderer.rb index 48707bfe..8bce94c6 100644 --- a/lib/jbuilder/collection_renderer.rb +++ b/lib/jbuilder/collection_renderer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'delegate' require 'active_support/concern' require 'action_view' diff --git a/lib/jbuilder/errors.rb b/lib/jbuilder/errors.rb index 386e6da8..e2de671c 100644 --- a/lib/jbuilder/errors.rb +++ b/lib/jbuilder/errors.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'jbuilder/jbuilder' class Jbuilder diff --git a/lib/jbuilder/jbuilder.rb b/lib/jbuilder/jbuilder.rb index 22b0ac4e..1fc30257 100644 --- a/lib/jbuilder/jbuilder.rb +++ b/lib/jbuilder/jbuilder.rb @@ -1 +1,3 @@ +# frozen_string_literal: true + Jbuilder = Class.new(BasicObject) diff --git a/lib/jbuilder/jbuilder_dependency_tracker.rb b/lib/jbuilder/jbuilder_dependency_tracker.rb index 62b6dbf2..b37fade0 100644 --- a/lib/jbuilder/jbuilder_dependency_tracker.rb +++ b/lib/jbuilder/jbuilder_dependency_tracker.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Jbuilder::DependencyTracker EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/ diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 55f2d5ff..46955c83 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'jbuilder/jbuilder' require 'jbuilder/collection_renderer' require 'action_dispatch/http/mime_type' diff --git a/lib/jbuilder/key_formatter.rb b/lib/jbuilder/key_formatter.rb index c9e7eca3..9e53d05b 100644 --- a/lib/jbuilder/key_formatter.rb +++ b/lib/jbuilder/key_formatter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'jbuilder/jbuilder' require 'active_support/core_ext/array' diff --git a/lib/jbuilder/railtie.rb b/lib/jbuilder/railtie.rb index 2aeefbb6..1b7c4127 100644 --- a/lib/jbuilder/railtie.rb +++ b/lib/jbuilder/railtie.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rails' require 'jbuilder/jbuilder_template' diff --git a/lib/jbuilder/version.rb b/lib/jbuilder/version.rb index 2a62782f..3fe2e274 100644 --- a/lib/jbuilder/version.rb +++ b/lib/jbuilder/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class Jbuilder VERSION = "2.13.0" end From ce9bfecff3d6fd66515604d8f87b71ee3b496792 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 21 Jul 2025 12:38:22 -0400 Subject: [PATCH 28/43] Update lib/jbuilder/railtie.rb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Rafael Mendonça França --- lib/jbuilder/railtie.rb | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/jbuilder/railtie.rb b/lib/jbuilder/railtie.rb index 490ad2f0..1e1cf9b0 100644 --- a/lib/jbuilder/railtie.rb +++ b/lib/jbuilder/railtie.rb @@ -15,11 +15,9 @@ module ApiRendering end end - ActiveSupport.on_load :action_controller do - if name == 'ActionController::API' - include ActionController::Helpers - include ActionController::ImplicitRender - end + ActiveSupport.on_load :action_controller_api do + include ActionController::Helpers + include ActionController::ImplicitRender end end From 64204f4a8b9b32b515070f90ccc6715675f068e2 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 21 Jul 2025 13:54:22 -0400 Subject: [PATCH 29/43] Optimize method_missing via alias_method --- lib/jbuilder.rb | 10 ++-------- lib/jbuilder/jbuilder_template.rb | 2 ++ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index 0ed3cb1e..024e418c 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -70,14 +70,6 @@ def set!(key, value = BLANK, *args, &block) _set_value key, result end - def method_missing(*args, &block) - if ::Kernel.block_given? - set!(*args, &block) - else - set!(*args) - end - end - # Specifies formatting to be applied to the key. Passing in a name of a function # will cause that function to be called on the key. So :upcase will upper case # the key. You can also pass in lambdas for more complex transformations. @@ -283,6 +275,8 @@ def target! private + alias_method :method_missing, :set! + def _extract(object, attributes) if ::Hash === object _extract_hash_values(object, attributes) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index fed4c8e9..b95e65f3 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -139,6 +139,8 @@ def set!(name, object = BLANK, *args) private + alias_method :method_missing, :set! + def _render_partial_with_options(options) options.reverse_merge! locals: options.except(:partial, :as, :collection, :cached) options.reverse_merge! ::JbuilderTemplate.template_lookup_options From 8f9993a3daadd60919a36c3df885fccd97992ce2 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 21 Jul 2025 14:59:54 -0400 Subject: [PATCH 30/43] Stop mutating options in array! method --- lib/jbuilder/jbuilder_template.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index e165ef6f..44219f33 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -121,8 +121,9 @@ def array!(collection = [], *args) options = args.first if args.one? && _partial_options?(options) + options = options.dup options[:collection] = collection - partial! options + _render_partial_with_options options else super end From d23ebe2c3f9d5231262c58d911461bce19812a25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Mon, 21 Jul 2025 19:09:37 +0000 Subject: [PATCH 31/43] Add devcontainer --- .devcontainer/devcontainer.json | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..96f29c19 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/ruby +{ + "name": "jbuilder", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "ghcr.io/rails/devcontainer/images/ruby:3.4.5", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {} + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "ruby --version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} From 7ea07fd12c324e41fec19709514e8b3112191506 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Wed, 18 Sep 2024 11:10:39 +0100 Subject: [PATCH 32/43] Fix warning about routes.rb not existing The controller scaffold generator tries to insert routes but there isn't a routes file the tmp folder so it prints a warning instead. Prevent the warning from being printed by using the option to skip routes. --- test/scaffold_api_controller_generator_test.rb | 6 +++--- test/scaffold_controller_generator_test.rb | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/scaffold_api_controller_generator_test.rb b/test/scaffold_api_controller_generator_test.rb index 8ab47082..546749b1 100644 --- a/test/scaffold_api_controller_generator_test.rb +++ b/test/scaffold_api_controller_generator_test.rb @@ -4,7 +4,7 @@ class ScaffoldApiControllerGeneratorTest < Rails::Generators::TestCase tests Rails::Generators::ScaffoldControllerGenerator - arguments %w(Post title body:text images:attachments --api) + arguments %w(Post title body:text images:attachments --api --skip-routes) destination File.expand_path('../tmp', __FILE__) setup :prepare_destination @@ -53,7 +53,7 @@ class ScaffoldApiControllerGeneratorTest < Rails::Generators::TestCase end test "don't use require and permit if there are no attributes" do - run_generator %w(Post --api) + run_generator %w(Post --api --skip-routes) assert_file 'app/controllers/posts_controller.rb' do |content| assert_match %r{def post_params}, content @@ -62,7 +62,7 @@ class ScaffoldApiControllerGeneratorTest < Rails::Generators::TestCase end test 'handles virtual attributes' do - run_generator ["Message", "content:rich_text", "video:attachment", "photos:attachments"] + run_generator %w(Message content:rich_text video:attachment photos:attachments --skip-routes) assert_file 'app/controllers/messages_controller.rb' do |content| if Rails::VERSION::MAJOR >= 8 diff --git a/test/scaffold_controller_generator_test.rb b/test/scaffold_controller_generator_test.rb index 795dec8e..048df2a3 100644 --- a/test/scaffold_controller_generator_test.rb +++ b/test/scaffold_controller_generator_test.rb @@ -4,7 +4,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase tests Rails::Generators::ScaffoldControllerGenerator - arguments %w(Post title body:text images:attachments) + arguments %w(Post title body:text images:attachments --skip-routes) destination File.expand_path('../tmp', __FILE__) setup :prepare_destination @@ -67,7 +67,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase end test 'controller with namespace' do - run_generator %w(Admin::Post --model-name=Post) + run_generator %w(Admin::Post --model-name=Post --skip-routes) assert_file 'app/controllers/admin/posts_controller.rb' do |content| assert_instance_method :create, content do |m| assert_match %r{format\.html \{ redirect_to \[:admin, @post\], notice: "Post was successfully created\." \}}, m @@ -84,7 +84,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase end test "don't use require and permit if there are no attributes" do - run_generator %w(Post) + run_generator %w(Post --skip-routes) assert_file 'app/controllers/posts_controller.rb' do |content| assert_match %r{def post_params}, content @@ -93,7 +93,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase end test 'handles virtual attributes' do - run_generator %w(Message content:rich_text video:attachment photos:attachments) + run_generator %w(Message content:rich_text video:attachment photos:attachments --skip-routes) assert_file 'app/controllers/messages_controller.rb' do |content| if Rails::VERSION::MAJOR >= 8 From b58d524c59cb6d9aa81efb11d8e4c2c41ba40f9e Mon Sep 17 00:00:00 2001 From: Andrew White Date: Wed, 18 Sep 2024 11:13:43 +0100 Subject: [PATCH 33/43] Use the correct path for the env command --- bin/test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/test b/bin/test index 45d4fe95..1bc5120f 100755 --- a/bin/test +++ b/bin/test @@ -1,4 +1,4 @@ -#!/bin/env bash +#!/usr/bin/env bash set -e bundle install From 1a86eb8d94967fcc04cf90282f7e071b87028ef2 Mon Sep 17 00:00:00 2001 From: Andrew White Date: Wed, 18 Sep 2024 11:28:07 +0100 Subject: [PATCH 34/43] Add Rails 7.2 to the Appraisals file --- .github/workflows/ruby.yml | 5 +++++ Appraisals | 4 ++++ gemfiles/rails_7_2.gemfile | 10 ++++++++++ 3 files changed, 19 insertions(+) create mode 100644 gemfiles/rails_7_2.gemfile diff --git a/.github/workflows/ruby.yml b/.github/workflows/ruby.yml index 0d5374dd..3bc499ad 100644 --- a/.github/workflows/ruby.yml +++ b/.github/workflows/ruby.yml @@ -24,14 +24,19 @@ jobs: gemfile: - "rails_7_0" - "rails_7_1" + - "rails_7_2" - "rails_8_0" - "rails_head" exclude: + - ruby: '3.0' + gemfile: rails_7_2 - ruby: '3.0' gemfile: rails_8_0 - ruby: '3.0' gemfile: rails_head + - ruby: '3.1' + gemfile: rails_7_2 - ruby: '3.1' gemfile: rails_8_0 - ruby: '3.1' diff --git a/Appraisals b/Appraisals index 3d1832d2..3caeb7ef 100644 --- a/Appraisals +++ b/Appraisals @@ -9,6 +9,10 @@ appraise "rails-7-1" do gem "rails", "~> 7.1.0" end +appraise "rails-7-2" do + gem "rails", "~> 7.2.0" +end + appraise "rails-8-0" do gem "rails", "~> 8.0.0" end diff --git a/gemfiles/rails_7_2.gemfile b/gemfiles/rails_7_2.gemfile new file mode 100644 index 00000000..8f5a412b --- /dev/null +++ b/gemfiles/rails_7_2.gemfile @@ -0,0 +1,10 @@ +# This file was generated by Appraisal + +source "https://rubygems.org" + +gem "rake" +gem "mocha", require: false +gem "appraisal" +gem "rails", "~> 7.2.0" + +gemspec path: "../" From 13e033b7de6cfaeb643995436c9dc39ea63b715a Mon Sep 17 00:00:00 2001 From: Andrew White Date: Wed, 18 Sep 2024 11:32:36 +0100 Subject: [PATCH 35/43] Fix method redefinition warning --- test/test_helper.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_helper.rb b/test/test_helper.rb index d985da9c..0b4dfa64 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -19,7 +19,7 @@ ENV["RAILS_ENV"] ||= "test" class << Rails - def cache + redefine_method :cache do @cache ||= ActiveSupport::Cache::MemoryStore.new end end From f447b6035abf83decc3afd98ae123b704af6951e Mon Sep 17 00:00:00 2001 From: Andrew White Date: Wed, 18 Sep 2024 11:33:38 +0100 Subject: [PATCH 36/43] Fix constant redefinition warning In e18fe2a the Jbuilder::VERSION constant was introduced but in 9aa3dd9 it was used in the gemspec which changed the loading order so that the version constant was loaded first. This defined Jbuilder as an Object subclass rather than the intended BasicObject and when jbuilder/jbuilder was required it redefined the Jbuilder constant and obliterates the VERSION constant. This commit ensures that the version constant exists and the Jbuilder parent class is BasicObject. --- lib/jbuilder.rb | 1 - lib/jbuilder/errors.rb | 2 +- lib/jbuilder/jbuilder.rb | 2 +- lib/jbuilder/version.rb | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index 024e418c..edf048c7 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -5,7 +5,6 @@ require 'jbuilder/blank' require 'jbuilder/key_formatter' require 'jbuilder/errors' -require 'jbuilder/version' require 'json' require 'active_support/core_ext/hash/deep_merge' diff --git a/lib/jbuilder/errors.rb b/lib/jbuilder/errors.rb index e2de671c..aba1f89b 100644 --- a/lib/jbuilder/errors.rb +++ b/lib/jbuilder/errors.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'jbuilder/jbuilder' +require 'jbuilder/version' class Jbuilder class NullError < ::NoMethodError diff --git a/lib/jbuilder/jbuilder.rb b/lib/jbuilder/jbuilder.rb index 1fc30257..ddb52733 100644 --- a/lib/jbuilder/jbuilder.rb +++ b/lib/jbuilder/jbuilder.rb @@ -1,3 +1,3 @@ # frozen_string_literal: true -Jbuilder = Class.new(BasicObject) +require 'jbuilder/version' diff --git a/lib/jbuilder/version.rb b/lib/jbuilder/version.rb index 3fe2e274..8d4d5e2d 100644 --- a/lib/jbuilder/version.rb +++ b/lib/jbuilder/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -class Jbuilder +class Jbuilder < BasicObject VERSION = "2.13.0" end From 8474b41f666b13055b9368107c783cdae7903fb6 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Mon, 21 Jul 2025 15:31:57 -0400 Subject: [PATCH 37/43] Remove _partial micro-optimization --- lib/jbuilder/jbuilder_template.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 44219f33..1d1afdc1 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -178,7 +178,7 @@ def _render_partial_with_options(options) def _render_partial(options) options[:locals][:json] = self - @context.render options, nil + @context.render options end def _cache_fragment_for(key, options, &block) From 7e16adf446c9da701e1e5fdbe9c93f7f8095630d Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 23 Jul 2025 14:05:41 -0400 Subject: [PATCH 38/43] Stop mutating options in set! method --- lib/jbuilder/jbuilder_template.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 1d1afdc1..5b64e5a4 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -133,7 +133,7 @@ def set!(name, object = BLANK, *args) options = args.first if args.one? && _partial_options?(options) - _set_inline_partial name, object, options + _set_inline_partial name, object, options.dup else super end From b7b5abb02d9bb2ed1fc584a08142838a05cafc08 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 23 Jul 2025 14:05:51 -0400 Subject: [PATCH 39/43] Stop mutating options in partial! method --- lib/jbuilder/jbuilder_template.rb | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index 5b64e5a4..a44cc2c2 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -55,7 +55,9 @@ def partial!(*args) if args.one? && _is_active_model?(args.first) _render_active_model_partial args.first else - _render_explicit_partial(*args) + options = args.extract_options!.dup + options[:partial] = args.first if args.present? + _render_partial_with_options options end end @@ -248,28 +250,6 @@ def _set_inline_partial(name, object, options) _set_value name, value end - def _render_explicit_partial(name_or_options, locals = {}) - case name_or_options - when ::Hash - # partial! partial: 'name', foo: 'bar' - options = name_or_options - else - # partial! 'name', locals: {foo: 'bar'} - if locals.one? && (locals.keys.first == :locals) - locals[:partial] = name_or_options - options = locals - else - options = { partial: name_or_options, locals: locals } - end - # partial! 'name', foo: 'bar' - as = locals.delete(:as) - options[:as] = as if as.present? - options[:collection] = locals[:collection] if locals.key?(:collection) - end - - _render_partial_with_options options - end - def _render_active_model_partial(object) @context.render object, json: self end From 6fd6c0662f6507be951ba842857c663adb000f56 Mon Sep 17 00:00:00 2001 From: Michael Oberegger Date: Wed, 23 Jul 2025 14:07:19 -0400 Subject: [PATCH 40/43] Small _set_inline_partial optimization --- lib/jbuilder/jbuilder_template.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/jbuilder/jbuilder_template.rb b/lib/jbuilder/jbuilder_template.rb index a44cc2c2..855dd678 100644 --- a/lib/jbuilder/jbuilder_template.rb +++ b/lib/jbuilder/jbuilder_template.rb @@ -240,9 +240,8 @@ def _set_inline_partial(name, object, options) _render_partial_with_options options end else - locals = ::Hash[options[:as], object] _scope do - options[:locals] = locals + options[:locals] = { options[:as] => object } _render_partial_with_options options end end From 30ba7df152aacdf20b6267a82b9d2b1f9e947636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Fri, 8 Aug 2025 19:46:18 +0000 Subject: [PATCH 41/43] Prepare for 2.14.0 --- lib/jbuilder/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jbuilder/version.rb b/lib/jbuilder/version.rb index 8d4d5e2d..79d5e2ba 100644 --- a/lib/jbuilder/version.rb +++ b/lib/jbuilder/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class Jbuilder < BasicObject - VERSION = "2.13.0" + VERSION = "2.14.0" end From a6863b5d582b966fcfbef8686c829c950efd1f5c Mon Sep 17 00:00:00 2001 From: Mike Dalessio Date: Tue, 12 Aug 2025 09:58:22 -0400 Subject: [PATCH 42/43] Ensure that Jbuilder.encode properly forwards arguments to .new The initializer previously accepted an options hash, but now takes kwargs. So the encode method also needs to accept (and forward) kwargs. --- lib/jbuilder.rb | 4 ++-- test/jbuilder_test.rb | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/jbuilder.rb b/lib/jbuilder.rb index edf048c7..1290839f 100644 --- a/lib/jbuilder.rb +++ b/lib/jbuilder.rb @@ -28,8 +28,8 @@ def initialize( end # Yields a builder and automatically turns the result into a JSON string - def self.encode(*args, &block) - new(*args, &block).target! + def self.encode(...) + new(...).target! end BLANK = Blank.new diff --git a/test/jbuilder_test.rb b/test/jbuilder_test.rb index eec22f17..c01fb1e8 100644 --- a/test/jbuilder_test.rb +++ b/test/jbuilder_test.rb @@ -936,4 +936,11 @@ class JbuilderTest < ActiveSupport::TestCase result = JSON.load(Jbuilder.encode { |json| json.time Time.parse("2018-05-13 11:51:00.485 -0400") }) assert_equal "2018-05-13T11:51:00.485-04:00", result["time"] end + + test "encode forwards options to new" do + Jbuilder.encode(key_formatter: 1, ignore_nil: 2) do |json| + assert_equal 1, json.instance_eval{ @key_formatter } + assert_equal 2, json.instance_eval{ @ignore_nil } + end + end end From 38339adaa9d44ba89c0dde2a795338a886941e6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafael=20Mendon=C3=A7a=20Fran=C3=A7a?= Date: Tue, 12 Aug 2025 15:37:46 +0000 Subject: [PATCH 43/43] Prepare for 2.14.1 --- lib/jbuilder/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jbuilder/version.rb b/lib/jbuilder/version.rb index 79d5e2ba..78151fd0 100644 --- a/lib/jbuilder/version.rb +++ b/lib/jbuilder/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true class Jbuilder < BasicObject - VERSION = "2.14.0" + VERSION = "2.14.1" end