diff --git a/lib/multi_fetch_fragments.rb b/lib/multi_fetch_fragments.rb index db36d04..1966263 100644 --- a/lib/multi_fetch_fragments.rb +++ b/lib/multi_fetch_fragments.rb @@ -3,92 +3,159 @@ module MultiFetchFragments included do alias_method_chain :render_collection, :multi_fetch_cache + alias_method_chain :retrieve_template_keys, :multi_fetch_cache + alias_method_chain :collection_with_template, :multi_fetch_cache + alias_method_chain :collection_without_template, :multi_fetch_cache end private - def render_collection_with_multi_fetch_cache - return nil if @collection.blank? + def cache_safe_variable_counter + ('cache_safe_' + @variable_counter.to_s).to_sym + end + + def retrieve_template_keys_with_multi_fetch_cache + keys = @locals.keys + keys << @variable if @object || @collection + keys << @variable_counter if @collection + keys << cache_safe_variable_counter if @collection + keys + end - if @options.key?(:spacer_template) - spacer = find_template(@options[:spacer_template]).render(@view, @locals) + def collection_with_template_with_multi_fetch_cache + view, locals, template = @view, @locals, @template + as, counter = @variable, @variable_counter + collection_index_map = @collection_index_map + + if layout = @options[:layout] + layout = find_template(layout, @template_keys) + end + + index = -1 + @collection.map do |object| + locals[as] = object + locals[counter] = (index += 1) + + if collection_index_map + locals[cache_safe_variable_counter] = collection_index_map[object] end - results = [] + content = template.render(view, locals) + content = layout.render(view, locals) { content } if layout + content + end + end - if cache_collection? + def collection_without_template_with_multi_fetch_cache + view, locals, collection_data = @view, @locals, @collection_data + collection_index_map = @collection_index_map + cache = {} + keys = @locals.keys - additional_cache_options = @options[:cache_options] || @locals[:cache_options] || {} - keys_to_collection_map = {} + index = -1 + @collection.map do |object| + index += 1 + path, as, counter = collection_data[index] - @collection.each do |item| - key = @options[:cache].respond_to?(:call) ? @options[:cache].call(item) : item + locals[as] = object + locals[counter] ||= index - key_with_optional_digest = nil - if defined?(@view.fragment_name_with_digest) - key_with_optional_digest = @view.fragment_name_with_digest(key, @view.view_cache_dependencies) - elsif defined?(@view.cache_fragment_name) - key_with_optional_digest = @view.cache_fragment_name(key) - else - key_with_optional_digest = key - end + if collection_index_map + locals[cache_safe_variable_counter] = collection_index_map[object] + end + template = (cache[path] ||= find_template(path, keys + [as, counter])) + template.render(view, locals) + end + end - expanded_key = fragment_cache_key(key_with_optional_digest) + def render_collection_with_multi_fetch_cache - keys_to_collection_map[expanded_key] = item - end + return nil if @collection.blank? - # cache.read_multi & cache.write interfaces may require mutable keys, ie. dalli 2.6.0 - mutable_keys = keys_to_collection_map.keys.collect { |key| key.dup } + if @options.key?(:spacer_template) + spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals) + end - result_hash = Rails.cache.read_multi(*mutable_keys) + results = [] - # if we had a cached value, we don't need to render that object from the collection. - # if it wasn't cached, we need to render those objects as before - @collection = (keys_to_collection_map.keys - result_hash.keys).map do |key| - keys_to_collection_map[key] - end + if cache_collection? - non_cached_results = [] + additional_cache_options = @options[:cache_options] || @locals[:cache_options] || {} - # sequentially render any non-cached objects remaining - if @collection.any? - non_cached_results = @template ? collection_with_template : collection_without_template - end + keys_to_collection_map = {} + @collection_index_map = {} - # sort the result according to the keys that were fed in, cache the non-cached results - mutable_keys.each do |key| + @collection.each_with_index do |item, index| + key = @options[:cache].respond_to?(:call) ? @options[:cache].call(item, index) : item - cached_value = result_hash[key] - if cached_value - results << cached_value - else - non_cached_result = non_cached_results.shift - Rails.cache.write(key, non_cached_result, additional_cache_options) + @collection_index_map[item] = index - results << non_cached_result - end + key_with_optional_digest = nil + if defined?(@view.fragment_name_with_digest) + key_with_optional_digest = @view.fragment_name_with_digest(key, @view.view_cache_dependencies) + elsif defined?(@view.cache_fragment_name) + key_with_optional_digest = @view.cache_fragment_name(key) + else + key_with_optional_digest = key end - else - results = @template ? collection_with_template : collection_without_template + expanded_key = fragment_cache_key(key_with_optional_digest) + + keys_to_collection_map[expanded_key] = item end - results.join(spacer).html_safe - end - def cache_collection? - cache_option = @options[:cache].presence || @locals[:cache].presence - ActionController::Base.perform_caching && cache_option - end + # cache.read_multi & cache.write interfaces may require mutable keys, ie. dalli 2.6.0 + mutable_keys = keys_to_collection_map.keys.collect { |key| key.dup } + + result_hash = Rails.cache.read_multi(*mutable_keys) + + # if we had a cached value, we don't need to render that object from the collection. + # if it wasn't cached, we need to render those objects as before + @collection = (keys_to_collection_map.keys - result_hash.keys).map do |key| + keys_to_collection_map[key] + end + + non_cached_results = [] + + # sequentially render any non-cached objects remaining + if @collection.any? + non_cached_results = @template ? collection_with_template : collection_without_template + end + + # sort the result according to the keys that were fed in, cache the non-cached results + mutable_keys.each do |key| + + cached_value = result_hash[key] + if cached_value + results << cached_value + else + non_cached_result = non_cached_results.shift + Rails.cache.write(key, non_cached_result, additional_cache_options) - # from Rails fragment_cache_key in ActionController::Caching::Fragments. Adding it here since it's tucked inside an instance method on the controller, and - # it's utility could be used in a view without a controller - def fragment_cache_key(key) - ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) + results << non_cached_result + end + end + + else + results = @template ? collection_with_template : collection_without_template end + results.join(spacer).html_safe + end + + def cache_collection? + cache_option = @options[:cache].presence || @locals[:cache].presence + ActionController::Base.perform_caching && cache_option + end + + # from Rails fragment_cache_key in ActionController::Caching::Fragments. Adding it here since it's tucked inside an instance method on the controller, and + # it's utility could be used in a view without a controller + def fragment_cache_key(key) + ActiveSupport::Cache.expand_cache_key(key.is_a?(Hash) ? url_for(key).split("://").last : key, :views) + end + class Railtie < Rails::Railtie initializer "multi_fetch_fragments.initialize" do |app| ActionView::PartialRenderer.class_eval do diff --git a/spec/multi_fetch_fragments_spec.rb b/spec/multi_fetch_fragments_spec.rb index b10e6aa..194e08d 100644 --- a/spec/multi_fetch_fragments_spec.rb +++ b/spec/multi_fetch_fragments_spec.rb @@ -8,9 +8,43 @@ view.render(:partial => "views/customer", :collection => [ Customer.new("david"), Customer.new("mary") ]).should == "Hello: david\nHello: mary\n" end + context "variant_counter" do + + it "does not break existing functionality" do + MultiFetchFragments::Railtie.run_initializers + + view = ActionView::Base.new([File.dirname(__FILE__)], {}) + view.render(:partial => "views/counter", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer).should == "Count: 0\nCount: 1\n" + end + + it "works for the cached version" do + cache_mock = double() + Rails.cache = cache_mock + MultiFetchFragments::Railtie.run_initializers + + controller = ActionController::Base.new + view = ActionView::Base.new([File.dirname(__FILE__)], {}, controller) + + david = Customer.new("david") + key1 = controller.fragment_cache_key([david, 'key']) + + mary = Customer.new("mary") + key2 = controller.fragment_cache_key([mary, 'key']) + + simon = Customer.new("simon") + key3 = controller.fragment_cache_key([simon, 'key']) + + cache_mock.should_receive(:read_multi).with(key1, key2, key3).and_return({key1 => "Count: 0, CacheSafeCount: 0\n"}) + cache_mock.should_receive(:write).twice + + view.render(:partial => "views/cache_safe_counter", :collection => [ david, mary, simon ], :cache => Proc.new{ |item| [item, 'key']}, :as => :customer).should == "Count: 0, CacheSafeCount: 0\nCount: 0, CacheSafeCount: 1\nCount: 1, CacheSafeCount: 2\n" + end + + end + it "works for passing in a custom key" do - cache_mock = mock() - RAILS_CACHE = cache_mock + cache_mock = double() + Rails.cache = cache_mock MultiFetchFragments::Railtie.run_initializers controller = ActionController::Base.new @@ -18,9 +52,11 @@ customer = Customer.new("david") key = controller.fragment_cache_key([customer, 'key']) - + cache_mock.should_receive(:read_multi).with(key).and_return({key => 'Hello'}) view.render(:partial => "views/customer", :collection => [ customer ], :cache => Proc.new{ |item| [item, 'key']}).should == "Hello" end + + end diff --git a/spec/views/_cache_safe_counter.html.erb b/spec/views/_cache_safe_counter.html.erb new file mode 100644 index 0000000..3fd9429 --- /dev/null +++ b/spec/views/_cache_safe_counter.html.erb @@ -0,0 +1 @@ +Count: <%= customer_counter %>, CacheSafeCount: <%= cache_safe_customer_counter %> diff --git a/spec/views/_counter.html.erb b/spec/views/_counter.html.erb new file mode 100644 index 0000000..a2727b1 --- /dev/null +++ b/spec/views/_counter.html.erb @@ -0,0 +1 @@ +Count: <%= customer_counter %>