Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 123 additions & 56 deletions lib/multi_fetch_fragments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
42 changes: 39 additions & 3 deletions spec/multi_fetch_fragments_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,55 @@
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
view = ActionView::Base.new([File.dirname(__FILE__)], {}, controller)

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
1 change: 1 addition & 0 deletions spec/views/_cache_safe_counter.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Count: <%= customer_counter %>, CacheSafeCount: <%= cache_safe_customer_counter %>
1 change: 1 addition & 0 deletions spec/views/_counter.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Count: <%= customer_counter %>