mirror of
https://github.com/Shopify/liquid.git
synced 2026-09-12 23:40:45 -07:00
Use sets to check if methods are invokable without symbolizing.
This commit is contained in:
@@ -39,6 +39,7 @@ module Liquid
|
|||||||
|
|
||||||
filters.each do |f|
|
filters.each do |f|
|
||||||
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
|
raise ArgumentError, "Expected module but got: #{f.class}" unless f.is_a?(Module)
|
||||||
|
Strainer.add_known_filter(f)
|
||||||
strainer.extend(f)
|
strainer.extend(f)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+7
-4
@@ -1,3 +1,5 @@
|
|||||||
|
require 'set'
|
||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
# A drop in liquid is a class which allows you to export DOM like things to liquid.
|
# A drop in liquid is a class which allows you to export DOM like things to liquid.
|
||||||
@@ -31,8 +33,8 @@ module Liquid
|
|||||||
|
|
||||||
# called by liquid to invoke a drop
|
# called by liquid to invoke a drop
|
||||||
def invoke_drop(method_or_key)
|
def invoke_drop(method_or_key)
|
||||||
if method_or_key && method_or_key != EMPTY_STRING && drop_method_defined?(method_or_key.to_s)
|
if method_or_key && method_or_key != EMPTY_STRING && self.class.invokable?(method_or_key)
|
||||||
send(method_or_key.to_s)
|
send(method_or_key)
|
||||||
else
|
else
|
||||||
before_method(method_or_key)
|
before_method(method_or_key)
|
||||||
end
|
end
|
||||||
@@ -51,8 +53,9 @@ module Liquid
|
|||||||
private
|
private
|
||||||
|
|
||||||
# Check for method existence without invoking respond_to?, which creates symbols
|
# Check for method existence without invoking respond_to?, which creates symbols
|
||||||
def drop_method_defined?(method_name)
|
def self.invokable?(method_name)
|
||||||
self.class.public_instance_methods.any? {|method| method.to_s == method_name }
|
@invokable_methods ||= Set.new((public_instance_methods - Liquid::Drop.public_instance_methods).map(&:to_s))
|
||||||
|
@invokable_methods.include?(method_name.to_s)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+17
-16
@@ -2,19 +2,15 @@ require 'set'
|
|||||||
|
|
||||||
module Liquid
|
module Liquid
|
||||||
|
|
||||||
parent_object = if defined? BlankObject
|
|
||||||
BlankObject
|
|
||||||
else
|
|
||||||
Object
|
|
||||||
end
|
|
||||||
|
|
||||||
# Strainer is the parent class for the filters system.
|
# Strainer is the parent class for the filters system.
|
||||||
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
# New filters are mixed into the strainer class which is then instantiated for each liquid template render run.
|
||||||
#
|
#
|
||||||
# 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 < parent_object #:nodoc:
|
class Strainer #:nodoc:
|
||||||
@@filters = {}
|
@@filters = {}
|
||||||
|
@@known_filters = Set.new
|
||||||
|
@@known_methods = Set.new
|
||||||
|
|
||||||
def initialize(context)
|
def initialize(context)
|
||||||
@context = context
|
@context = context
|
||||||
@@ -22,9 +18,20 @@ module Liquid
|
|||||||
|
|
||||||
def self.global_filter(filter)
|
def self.global_filter(filter)
|
||||||
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
|
raise ArgumentError, "Passed filter is not a module" unless filter.is_a?(Module)
|
||||||
|
add_known_filter(filter)
|
||||||
@@filters[filter.name] = filter
|
@@filters[filter.name] = filter
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self.add_known_filter(filter)
|
||||||
|
unless @@known_filters.include?(filter)
|
||||||
|
@@method_blacklist ||= Set.new(Strainer.instance_methods.map(&:to_s))
|
||||||
|
new_methods = filter.instance_methods.map(&:to_s)
|
||||||
|
new_methods.reject!{ |m| @@method_blacklist.include?(m) }
|
||||||
|
@@known_methods.merge(new_methods)
|
||||||
|
@@known_filters.add(filter)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def self.create(context)
|
def self.create(context)
|
||||||
strainer = Strainer.new(context)
|
strainer = Strainer.new(context)
|
||||||
@@filters.each { |k,m| strainer.extend(m) }
|
@@filters.each { |k,m| strainer.extend(m) }
|
||||||
@@ -32,21 +39,15 @@ module Liquid
|
|||||||
end
|
end
|
||||||
|
|
||||||
def invoke(method, *args)
|
def invoke(method, *args)
|
||||||
if has_method?(method)
|
if invokable?(method)
|
||||||
send(method, *args)
|
send(method, *args)
|
||||||
else
|
else
|
||||||
args.first
|
args.first
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
def invokable?(method)
|
||||||
|
@@known_methods.include?(method.to_s) && respond_to?(method)
|
||||||
def has_method?(method)
|
|
||||||
methods_to_check = self.methods - self.class.public_instance_methods
|
|
||||||
methods_to_check.any? do |instance_method|
|
|
||||||
instance_method.to_s == method.to_s
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -115,6 +115,13 @@ class DropsTest < Test::Unit::TestCase
|
|||||||
assert_equal ' ', output
|
assert_equal ' ', output
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_object_methods_not_allowed
|
||||||
|
[:dup, :clone, :singleton_class, :eval, :class_eval, :inspect].each do |method|
|
||||||
|
output = Liquid::Template.parse(" {{ product.#{method} }} ").render('product' => ProductDrop.new)
|
||||||
|
assert_equal ' ', output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def test_scope
|
def test_scope
|
||||||
assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new)
|
assert_equal '1', Liquid::Template.parse( '{{ context.scopes }}' ).render('context' => ContextDrop.new)
|
||||||
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
|
assert_equal '2', Liquid::Template.parse( '{%for i in dummy%}{{ context.scopes }}{%endfor%}' ).render('context' => ContextDrop.new, 'dummy' => [1])
|
||||||
|
|||||||
@@ -14,22 +14,21 @@ class StrainerTest < Test::Unit::TestCase
|
|||||||
private :private_filter
|
private :private_filter
|
||||||
end
|
end
|
||||||
|
|
||||||
module TestingFilter
|
|
||||||
def test1
|
|
||||||
"test1"
|
|
||||||
end
|
|
||||||
|
|
||||||
def test2
|
|
||||||
"test2"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Strainer.global_filter(AccessScopeFilters)
|
Strainer.global_filter(AccessScopeFilters)
|
||||||
Strainer.global_filter(TestingFilter)
|
|
||||||
|
def test_strainer
|
||||||
|
strainer = Strainer.create(nil)
|
||||||
|
assert_equal 5, strainer.invoke('size', 'input')
|
||||||
|
assert_equal "public", strainer.invoke("public_filter")
|
||||||
|
end
|
||||||
|
|
||||||
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 "public", strainer.invoke("public_filter")
|
assert_equal false, strainer.invokable?('__test__')
|
||||||
|
assert_equal false, strainer.invokable?('test')
|
||||||
|
assert_equal false, strainer.invokable?('instance_eval')
|
||||||
|
assert_equal false, strainer.invokable?('__send__')
|
||||||
|
assert_equal true, strainer.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
|
||||||
@@ -43,12 +42,6 @@ class StrainerTest < Test::Unit::TestCase
|
|||||||
assert_equal "password", strainer.invoke("undef_the_method", "password")
|
assert_equal "password", strainer.invoke("undef_the_method", "password")
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_strainer_allows_multiple_filters
|
|
||||||
strainer = Strainer.create(nil)
|
|
||||||
assert_equal "test1", strainer.invoke("test1")
|
|
||||||
assert_equal "test2", strainer.invoke("test2")
|
|
||||||
end
|
|
||||||
|
|
||||||
def test_strainer_only_allows_methods_defined_in_filters
|
def test_strainer_only_allows_methods_defined_in_filters
|
||||||
strainer = Strainer.create(nil)
|
strainer = Strainer.create(nil)
|
||||||
assert_equal "1 + 1", strainer.invoke("instance_eval", "1 + 1")
|
assert_equal "1 + 1", strainer.invoke("instance_eval", "1 + 1")
|
||||||
|
|||||||
Reference in New Issue
Block a user