diff --git a/jbuilder.gemspec b/jbuilder.gemspec index 0b51953..8a2580a 100644 --- a/jbuilder.gemspec +++ b/jbuilder.gemspec @@ -9,10 +9,10 @@ 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', '>= 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') diff --git a/lib/jbuilder/collection_renderer.rb b/lib/jbuilder/collection_renderer.rb index 48707bf..94b2fdf 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 55f2d5f..1326c9d 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/lib/jbuilder/railtie.rb b/lib/jbuilder/railtie.rb index 2aeefbb..490ad2f 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 diff --git a/test/jbuilder_generator_test.rb b/test/jbuilder_generator_test.rb index e4a2f16..8b1ab9a 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/jbuilder_template_test.rb b/test/jbuilder_template_test.rb index 54addb2..76368e1 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) - 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" - } + 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" } + } - 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 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 "The `:layout' option is not supported in collection rendering.", error.message + 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 @@ -427,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 diff --git a/test/jbuilder_test.rb b/test/jbuilder_test.rb index 76569bb..29a4d6b 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 diff --git a/test/scaffold_api_controller_generator_test.rb b/test/scaffold_api_controller_generator_test.rb index e7e2b35..8ab4708 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 a642eef..795dec8 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