From 4be63a7cd465940f54cc39c5668091fa4cd3a5f5 Mon Sep 17 00:00:00 2001 From: Stanislav Katkov Date: Thu, 2 Nov 2023 13:49:38 +0100 Subject: [PATCH] fragment caching implementation --- lib/pbbuilder/template.rb | 71 +++++++++++++++++++-------------- test/pbbuilder_template_test.rb | 15 +++++++ 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/lib/pbbuilder/template.rb b/lib/pbbuilder/template.rb index d865a65..a3ce4be 100644 --- a/lib/pbbuilder/template.rb +++ b/lib/pbbuilder/template.rb @@ -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) @@ -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 @@ -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 diff --git a/test/pbbuilder_template_test.rb b/test/pbbuilder_template_test.rb index ef93c13..b6cb2a8 100644 --- a/test/pbbuilder_template_test.rb +++ b/test/pbbuilder_template_test.rb @@ -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", [])]')