Skip to content

Commit

Permalink
Speed up collection rendering and support collection caching
Browse files Browse the repository at this point in the history
  • Loading branch information
yuki24 committed Feb 24, 2021
1 parent 770abdb commit 4a0a64a
Show file tree
Hide file tree
Showing 4 changed files with 208 additions and 7 deletions.
117 changes: 117 additions & 0 deletions lib/jbuilder/collection_renderer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
require 'delegate'

begin
require 'action_view/renderer/collection_renderer'
rescue LoadError
require 'action_view/renderer/partial_renderer'
end

class Jbuilder
module CollectionRenderable
extend ActiveSupport::Concern

class_methods do
def supports_collection_rendering?
superclass.private_method_defined?(:build_rendered_template) && 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 ScopedIterator < ::SimpleDelegator # :nodoc:
include Enumerable

def initialize(obj, scope)
super(obj)
@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?

__getobj__.each_with_info do |object, (path, as, counter, iteration)|
@scope.call { yield(object, [path, as, counter, iteration]) }
end
end
end

private_constant :ScopedIterator
end

collection_renderer_available = begin
::ActionView::CollectionRenderer

true
rescue ::NameError
false
end

if collection_renderer_available
# Rails 6.1 support:
class CollectionRenderer < ::ActionView::CollectionRenderer
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
end
else
# Rails 6.0 support:
class CollectionRenderer < ::ActionView::PartialRenderer
include CollectionRenderable

def initialize(lookup_context, options, &scope)
super(lookup_context)
@options = options
@scope = scope
end

def render_collection_with_partial(collection, partial, context, block)
render(context, @options.merge(collection: collection, partial: partial), block)
end

private

def collection_without_template(view)
@collection = ScopedIterator.new(@collection, @scope)

super(view)
end

def collection_with_template(view, template)
@collection = ScopedIterator.new(@collection, @scope)

super(view, template)
end
end
end
end
24 changes: 22 additions & 2 deletions lib/jbuilder/jbuilder_template.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'jbuilder/jbuilder'
require 'jbuilder/collection_renderer'
require 'action_dispatch/http/mime_type'
require 'active_support/cache'

Expand Down Expand Up @@ -104,11 +105,30 @@ def set!(name, object = BLANK, *args)
private

def _render_partial_with_options(options)
options.reverse_merge! locals: options.except(:partial, :as, :collection)
options.reverse_merge! locals: options.except(:partial, :as, :collection, :cached)
options.reverse_merge! ::JbuilderTemplate.template_lookup_options
as = options[:as]

if as && options.key?(:collection)
if options.key?(:collection) && options[:collection].nil?
array!
elsif as && options.key?(:collection) && CollectionRenderer.supports_collection_rendering?
collection = options.delete(:collection) || []
partial = options.delete(:partial)
options[:locals].merge!(json: self)

if options.has_key?(:layout)
raise ::NotImplementedError, "The `:layout' option is not supported in collection rendering."
end

if options.has_key?(:spacer_template)
raise ::NotImplementedError, "The `:spacer_template' option is not supported in collection rendering."
end

CollectionRenderer
.new(@context.lookup_context, options) { |&block| _scope(&block) }
.render_collection_with_partial(collection, partial, @context, nil)
elsif as && options.key?(:collection) && !CollectionRenderer.supports_collection_rendering?
# For Rails <= 5.2:
as = as.to_sym
collection = options.delete(:collection)
locals = options.delete(:locals)
Expand Down
61 changes: 60 additions & 1 deletion test/jbuilder_template_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,51 @@ class JbuilderTemplateTest < ActiveSupport::TestCase
assert_equal "David", result["firstName"]
end

if JbuilderTemplate::CollectionRenderer.supports_collection_rendering?
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 expected, Rails.cache.read("post-1")

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

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

assert_equal "The `:layout' option is not supported in collection rendering.", error.message
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 `:spacer_template' option is not supported in collection rendering.", error.message
end
end

private
def render(*args)
JSON.load render_without_parsing(*args)
Expand All @@ -305,7 +350,21 @@ def build_view(options = {})
ActionView::Base.new(lookup_context, options.fetch(:assigns, {}), controller)
end

def view.view_cache_dependencies; []; end
def view.view_cache_dependencies
[]
end

def view.combined_fragment_cache_key(key)
[ key ]
end

def view.cache_fragment_name(key, *)
key
end

def view.fragment_name_with_digest(key)
key
end

view
end
Expand Down
13 changes: 9 additions & 4 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,19 @@ def cache
end
end

class Post < Struct.new(:id, :body, :author_name); end
if JbuilderTemplate::CollectionRenderer.supports_collection_rendering?
Jbuilder::CollectionRenderer.collection_cache = Rails.cache
end

class Post < Struct.new(:id, :body, :author_name)
def cache_key
"post-#{id}"
end
end

class Racer < Struct.new(:id, :name)
extend ActiveModel::Naming
include ActiveModel::Conversion
end

ActionView::Template.register_template_handler :jbuilder, JbuilderHandler

ActionView::Base.remove_possible_method :fragment_name_with_digest
ActionView::Base.remove_possible_method :cache_fragment_name

0 comments on commit 4a0a64a

Please sign in to comment.