diff --git a/lib/liquid/context.rb b/lib/liquid/context.rb index 452b750..2687dab 100644 --- a/lib/liquid/context.rb +++ b/lib/liquid/context.rb @@ -25,6 +25,7 @@ module Liquid squash_instance_assigns_with_environments @interrupts = [] + @filters = [] end def resource_limits_reached? @@ -34,7 +35,7 @@ module Liquid end def strainer - @strainer ||= Strainer.create(self) + @strainer ||= Strainer.create(self, @filters) end # Adds filters to this context. @@ -43,11 +44,20 @@ module Liquid # for that def add_filters(filters) filters = [filters].flatten.compact - filters.each do |f| raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module) Strainer.add_known_filter(f) - strainer.extend(f) + end + + # If strainer is already setup then there's no choice but to use a runtime + # extend call. If strainer is not yet created, we can utilize strainers + # cached class based API, which avoids busting the method cache. + if @strainer + filters.each do |f| + strainer.extend(f) + end + else + @filters.concat filters end end diff --git a/lib/liquid/strainer.rb b/lib/liquid/strainer.rb index 5e75cdd..f5d525d 100644 --- a/lib/liquid/strainer.rb +++ b/lib/liquid/strainer.rb @@ -11,6 +11,11 @@ module Liquid @@filters = [] @@known_filters = Set.new @@known_methods = Set.new + @@strainer_class_cache = Hash.new do |hash, filters| + hash[filters] = Class.new(Strainer) do + filters.each { |f| include f } + end + end def initialize(context) @context = context @@ -32,10 +37,13 @@ module Liquid end end - def self.create(context) - strainer = Strainer.new(context) - @@filters.each { |m| strainer.extend(m) } - strainer + def self.strainer_class_cache + @@strainer_class_cache + end + + def self.create(context, filters = nil) + filters = @@filters + (filters || []) + strainer_class_cache[filters].new(context) end def invoke(method, *args) diff --git a/test/liquid/strainer_test.rb b/test/liquid/strainer_test.rb index 582ed7f..b7ec593 100644 --- a/test/liquid/strainer_test.rb +++ b/test/liquid/strainer_test.rb @@ -49,4 +49,15 @@ class StrainerTest < Test::Unit::TestCase assert_equal "has_method?", strainer.invoke("invoke", "has_method?", "invoke") end + def test_strainer_uses_a_class_cache_to_avoid_method_cache_invalidation + a, b = Module.new, Module.new + strainer = Strainer.create(nil, [a,b]) + assert_kind_of Strainer, strainer + assert_kind_of a, strainer + assert_kind_of b, strainer + Strainer.send(:class_variable_get, :@@filters).each do |m| + assert_kind_of m, strainer + end + end + end # StrainerTest