Skip to content

Commit

Permalink
fragment caching implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Stanislav Katkov committed Nov 2, 2023
1 parent 347b949 commit 4be63a7
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 31 deletions.
71 changes: 40 additions & 31 deletions lib/pbbuilder/template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ def partial!(*args)
end
end

# Set value in a field in message.
#
# @example
# pb.friends @friends, partial: "friend", as: :friend
# pb.friends partial: "racers/racer", as: :racer, collection: [Racer.new(1, "Johnny Test", []), Racer.new(2, "Max Verstappen", [])]
# pb.best_friend partial: "person", person: @best_friend

def set!(field, *args, **kwargs, &block)
# If partial options are being passed, we render a submessage with a partial
if kwargs.has_key?(:partial)
Expand All @@ -41,37 +48,7 @@ def set!(field, *args, **kwargs, &block)
end
elsif kwargs.has_key?(:collection) && kwargs.has_key?(:as)
# pb.friends partial: "racers/racer", as: :racer, collection: [Racer.new(1, "Johnny Test", []), Racer.new(2, "Max Verstappen", [])]
# collection renderer
options = kwargs.deep_dup

options.reverse_merge! locals: options.except(:partial, :as, :collection, :cached)
options.reverse_merge! ::PbbuilderTemplate.template_lookup_options

collection = options[:collection] || []
partial = options[:partial]

# The way recursive rendering works is that CollectionRenderer needs to be aware of node its currently rendering and parent node,
# these is no need to know entire "stack" of nodes. CollectionRenderer would traverse to bottom node render that first and then go up in stack.

# CollectionRenderer uses locals[:pb] to render the partial as a protobuf message,
# but also needs locals[:pb_parent] to apply rendered partial to top level protobuf message.

# This logic could be found in CollectionRenderer#build_rendered_collection method that we over wrote.
options[:locals].merge!(pb: ::PbbuilderTemplate.new(@context, new_message_for(field)))
options[:locals].merge!(pb_parent: self)
options[:locals].merge!(field: field)

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(message[field.to_s],&block) }
.render_collection_with_partial(collection, partial, @context, nil)
_render_collection_with_options(field, kwargs[:collection], kwargs.deep_dup)
else
# pb.best_friend partial: "person", person: @best_friend
# Call set! as a submessage, passing in the kwargs as partial options
Expand Down Expand Up @@ -119,6 +96,38 @@ def cache_if!(condition, *args, &block)

private

# Uses ActionView::CollectionRenderer to render collection effectively (and users fragment caching)
#
# The way recursive rendering works is that CollectionRenderer needs to be aware of node its currently rendering and parent node,
# these is no need to know entire "stack" of nodes. CollectionRenderer would traverse to bottom node render that first and then go up in stack.

# CollectionRenderer uses locals[:pb] to render the partial as a protobuf message,
# but also needs locals[:pb_parent] to apply rendered partial to top level protobuf message.

# This logic could be found in CollectionRenderer#build_rendered_collection method that we over wrote.
def _render_collection_with_options(field, collection, options)
partial = options[:partial]

options.reverse_merge! locals: options.except(:partial, :as, :collection, :cached)
options.reverse_merge! ::PbbuilderTemplate.template_lookup_options

options[:locals].merge!(pb: ::PbbuilderTemplate.new(@context, new_message_for(field)))
options[:locals].merge!(pb_parent: self)
options[:locals].merge!(field: field)

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(message[field.to_s],&block) }
.render_collection_with_partial(collection, partial, @context, nil)
end

# Writes to cache, if cache with keys is missing.
#
# @return fragment value
Expand Down
15 changes: 15 additions & 0 deletions test/pbbuilder_template_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,21 @@ class PbbuilderTemplateTest < ActiveSupport::TestCase
assert_equal "https://google.com/test3.svg", result.friends.first.friends.first.friends.first.logo.url
end

test "partial by name with caching" do
template = <<-PBBUILDER
racers = [Racer.new(1, "Johnny Test", [], nil, API::Asset.new(url: "https://google.com/test1.svg")), Racer.new(2, "Max Verstappen", [])]
pb.friends partial: "racers/racer", as: :racer, collection: racers, cached: true
PBBUILDER

assert_difference('Rails.cache.instance_variable_get(:@data).size') do
result = render(template)
end

assert_equal 2, result.friends.count
assert_nil result.logo
assert_equal "https://google.com/test1.svg", result.friends.first.logo.url
end

test "CollectionRenderer: raises an error on a render with :layout option" do
error = assert_raises NotImplementedError do
render('pb.friends partial: "racers/racer", as: :racer, layout: "layout", collection: [Racer.new(1, "Johnny Test", []), Racer.new(2, "Max Verstappen", [])]')
Expand Down

0 comments on commit 4be63a7

Please sign in to comment.