Allow filters to redefine Object methods to make them invokable.

This commit is contained in:
Dylan Thacker-Smith
2015-02-03 13:51:33 -05:00
parent 3476a556dd
commit 950f062041
5 changed files with 50 additions and 50 deletions
+2 -15
View File
@@ -44,21 +44,8 @@ module Liquid
# for that # for that
def add_filters(filters) def add_filters(filters)
filters = [filters].flatten.compact filters = [filters].flatten.compact
filters.each do |f| @filters += filters
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module) @strainer = nil
Strainer.add_known_filter(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 end
# are there any not handled interrupts? # are there any not handled interrupts?
+21 -25
View File
@@ -8,12 +8,13 @@ module Liquid
# The Strainer only allows method calls defined in filters given to it via Strainer.global_filter, # The Strainer only allows method calls defined in filters given to it via Strainer.global_filter,
# Context#add_filters or Template.register_filter # Context#add_filters or Template.register_filter
class Strainer #:nodoc: class Strainer #:nodoc:
@@filters = [] @@global_strainer = Class.new(Strainer) do
@@known_filters = Set.new @filter_methods = Set.new
@@known_methods = Set.new end
@@strainer_class_cache = Hash.new do |hash, filters| @@strainer_class_cache = Hash.new do |hash, filters|
hash[filters] = Class.new(Strainer) do hash[filters] = Class.new(@@global_strainer) do
filters.each { |f| include f } @filter_methods = @@global_strainer.filter_methods.dup
filters.each { |f| add_filter(f) }
end end
end end
@@ -21,33 +22,32 @@ module Liquid
@context = context @context = context
end end
def self.global_filter(filter) def self.filter_methods
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module) @filter_methods
add_known_filter(filter)
@@filters << filter unless @@filters.include?(filter)
end end
def self.add_known_filter(filter) def self.add_filter(filter)
unless @@known_filters.include?(filter) raise ArgumentError, "Expected module but got: #{f.class}" unless filter.is_a?(Module)
@@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s)) unless self.class.include?(filter)
new_methods = filter.instance_methods.map(&:to_s) self.send(:include, filter)
new_methods.reject!{ |m| @@method_blacklist.include?(m) } @filter_methods.merge(filter.public_instance_methods.map(&:to_s))
@@known_methods.merge(new_methods)
@@known_filters.add(filter)
end end
end end
def self.strainer_class_cache def self.global_filter(filter)
@@strainer_class_cache @@global_strainer.add_filter(filter)
end
def self.invokable?(method)
@filter_methods.include?(method.to_s)
end end
def self.create(context, filters = []) def self.create(context, filters = [])
filters = @@filters + filters @@strainer_class_cache[filters].new(context)
strainer_class_cache[filters].new(context)
end end
def invoke(method, *args) def invoke(method, *args)
if invokable?(method) if self.class.invokable?(method)
send(method, *args) send(method, *args)
else else
args.first args.first
@@ -55,9 +55,5 @@ module Liquid
rescue ::ArgumentError => e rescue ::ArgumentError => e
raise Liquid::ArgumentError.new(e.message) raise Liquid::ArgumentError.new(e.message)
end end
def invokable?(method)
@@known_methods.include?(method.to_s) && respond_to?(method)
end
end end
end end
+13
View File
@@ -25,6 +25,12 @@ end
class FiltersTest < Minitest::Test class FiltersTest < Minitest::Test
include Liquid include Liquid
module OverrideObjectMethodFilter
def tap(input)
"tap overridden"
end
end
def setup def setup
@context = Context.new @context = Context.new
end end
@@ -105,6 +111,13 @@ class FiltersTest < Minitest::Test
output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context) output = Variable.new(%! 'hello %{first_name}, %{last_name}' | substitute: first_name: surname, last_name: 'doe' !).render(@context)
assert_equal 'hello john, doe', output assert_equal 'hello john, doe', output
end end
def test_override_object_method_in_filter
assert_equal "tap overridden", Template.parse("{{var | tap}}").render!({ 'var' => 1000 }, :filters => [OverrideObjectMethodFilter])
# tap still treated as a non-existent filter
assert_equal "1000", Template.parse("{{var | tap}}").render!({ 'var' => 1000 })
end
end end
class FiltersInTemplate < Minitest::Test class FiltersInTemplate < Minitest::Test
+8 -2
View File
@@ -49,13 +49,19 @@ module Minitest
end end
def with_global_filter(*globals) def with_global_filter(*globals)
original_filters = Array.new(Liquid::Strainer.class_variable_get(:@@filters)) original_global_strainer = Liquid::Strainer.class_variable_get(:@@global_strainer)
Liquid::Strainer.class_variable_set(:@@global_strainer, Class.new(Liquid::Strainer) do
@filter_methods = Set.new
end)
Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear
globals.each do |global| globals.each do |global|
Liquid::Template.register_filter(global) Liquid::Template.register_filter(global)
end end
yield yield
ensure ensure
Liquid::Strainer.class_variable_set(:@@filters, original_filters) Liquid::Strainer.class_variable_get(:@@strainer_class_cache).clear
Liquid::Strainer.class_variable_set(:@@global_strainer, original_global_strainer)
end end
def with_taint_mode(mode) def with_taint_mode(mode)
+6 -8
View File
@@ -31,11 +31,11 @@ class StrainerUnitTest < Minitest::Test
def test_strainer_only_invokes_public_filter_methods def test_strainer_only_invokes_public_filter_methods
strainer = Strainer.create(nil) strainer = Strainer.create(nil)
assert_equal false, strainer.invokable?('__test__') assert_equal false, strainer.class.invokable?('__test__')
assert_equal false, strainer.invokable?('test') assert_equal false, strainer.class.invokable?('test')
assert_equal false, strainer.invokable?('instance_eval') assert_equal false, strainer.class.invokable?('instance_eval')
assert_equal false, strainer.invokable?('__send__') assert_equal false, strainer.class.invokable?('__send__')
assert_equal true, strainer.invokable?('size') # from the standard lib assert_equal true, strainer.class.invokable?('size') # from the standard lib
end end
def test_strainer_returns_nil_if_no_filter_method_found def test_strainer_returns_nil_if_no_filter_method_found
@@ -63,9 +63,7 @@ class StrainerUnitTest < Minitest::Test
assert_kind_of Strainer, strainer assert_kind_of Strainer, strainer
assert_kind_of a, strainer assert_kind_of a, strainer
assert_kind_of b, strainer assert_kind_of b, strainer
Strainer.class_variable_get(:@@filters).each do |m| assert_kind_of Liquid::StandardFilters, strainer
assert_kind_of m, strainer
end
end end
end # StrainerTest end # StrainerTest