[ruby/ostruct] Protect subclass' methods and our bang methods.

Internally, use only bang methods
This commit is contained in:
Marc-Andre Lafortune 2020-09-25 20:58:48 -04:00 коммит произвёл Marc-André Lafortune
Родитель df4d08c44a
Коммит 083fa6e5d2
2 изменённых файлов: 56 добавлений и 18 удалений

Просмотреть файл

@ -94,18 +94,15 @@
# o.class # => :luxury # o.class # => :luxury
# o.class! # => OpenStruct # o.class! # => OpenStruct
# #
# It is recommended (but not enforced) to not use fields ending in `!`. # It is recommended (but not enforced) to not use fields ending in `!`;
# Note that a subclass' methods may not be overwritten, nor can OpenStruct's own methods
# ending with `!`.
# #
# For all these reasons, consider not using OpenStruct at all. # For all these reasons, consider not using OpenStruct at all.
# #
class OpenStruct class OpenStruct
VERSION = "0.2.0" VERSION = "0.2.0"
instance_methods.each do |method|
new_name = "#{method}!"
alias_method new_name, method
end
# #
# Creates a new OpenStruct object. By default, the resulting OpenStruct # Creates a new OpenStruct object. By default, the resulting OpenStruct
# object will have no attributes. # object will have no attributes.
@ -164,7 +161,7 @@ class OpenStruct
# # => {"country" => "AUSTRALIA", "capital" => "CANBERRA" } # # => {"country" => "AUSTRALIA", "capital" => "CANBERRA" }
# #
def to_h(&block) def to_h(&block)
if block_given? if block
@table.to_h(&block) @table.to_h(&block)
else else
@table.dup @table.dup
@ -210,13 +207,23 @@ class OpenStruct
# define_singleton_method for both the getter method and the setter method. # define_singleton_method for both the getter method and the setter method.
# #
def new_ostruct_member!(name) # :nodoc: def new_ostruct_member!(name) # :nodoc:
unless @table.key?(name) unless @table.key?(name) || is_method_protected!(name)
define_singleton_method(name) { @table[name] } define_singleton_method!(name) { @table[name] }
define_singleton_method("#{name}=") {|x| @table[name] = x} define_singleton_method!("#{name}=") {|x| @table[name] = x}
end end
end end
private :new_ostruct_member! private :new_ostruct_member!
private def is_method_protected!(name) # :nodoc:
if !respond_to?(name, true)
false
elsif name.end_with?('!')
true
else
method!(name).owner < OpenStruct
end
end
def freeze def freeze
@table.freeze @table.freeze
super super
@ -226,18 +233,18 @@ class OpenStruct
len = args.length len = args.length
if mname = mid[/.*(?==\z)/m] if mname = mid[/.*(?==\z)/m]
if len != 1 if len != 1
raise ArgumentError, "wrong number of arguments (given #{len}, expected 1)", caller(1) raise! ArgumentError, "wrong number of arguments (given #{len}, expected 1)", caller(1)
end end
set_ostruct_member_value!(mname, args[0]) set_ostruct_member_value!(mname, args[0])
elsif len == 0 elsif len == 0
elsif @table.key?(mid) elsif @table.key?(mid)
raise ArgumentError, "wrong number of arguments (given #{len}, expected 0)" raise! ArgumentError, "wrong number of arguments (given #{len}, expected 0)"
else else
begin begin
super super
rescue NoMethodError => err rescue NoMethodError => err
err.backtrace.shift err.backtrace.shift
raise raise!
end end
end end
end end
@ -293,7 +300,7 @@ class OpenStruct
begin begin
name = name.to_sym name = name.to_sym
rescue NoMethodError rescue NoMethodError
raise TypeError, "#{name} is not a symbol nor a string" raise! TypeError, "#{name} is not a symbol nor a string"
end end
@table.dig(name, *names) @table.dig(name, *names)
end end
@ -321,7 +328,7 @@ class OpenStruct
rescue NameError rescue NameError
end end
@table.delete(sym) do @table.delete(sym) do
raise NameError.new("no field `#{sym}' in #{self}", sym) raise! NameError.new("no field `#{sym}' in #{self}", sym)
end end
end end
@ -344,13 +351,13 @@ class OpenStruct
ids.pop ids.pop
end end
end end
['#<', self.class, detail, '>'].join ['#<', self.class!, detail, '>'].join
end end
alias :to_s :inspect alias :to_s :inspect
attr_reader :table # :nodoc: attr_reader :table # :nodoc:
protected :table
alias table! table alias table! table
protected :table!
# #
# Compares this object and +other+ for equality. An OpenStruct is equal to # Compares this object and +other+ for equality. An OpenStruct is equal to
@ -388,4 +395,13 @@ class OpenStruct
def hash def hash
@table.hash @table.hash
end end
# Make all public methods (builtin or our own) accessible with `!`:
instance_methods.each do |method|
new_name = "#{method}!"
alias_method new_name, method
end
# Other builtin private methods we use:
alias_method :raise!, :raise
private :raise!
end end

Просмотреть файл

@ -255,8 +255,30 @@ class TC_OpenStruct < Test::Unit::TestCase
end end
def test_access_original_methods def test_access_original_methods
os = OpenStruct.new(method: :foo) os = OpenStruct.new(method: :foo, hash: 42)
assert_equal(os.object_id, os.method!(:object_id).call) assert_equal(os.object_id, os.method!(:object_id).call)
assert_not_equal(42, os.hash!)
end
def test_override_subclass
c = Class.new(OpenStruct) {
def foo; :protect_me; end
private def bar; :protect_me; end
def inspect; 'protect me'; end
}
o = c.new(
foo: 1, bar: 2, inspect: '3', # in subclass: protected
table!: 4, # bang method: protected
each_pair: 5, to_s: 'hello', # others: not protected
)
# protected:
assert_equal(:protect_me, o.foo)
assert_equal(:protect_me, o.send(:bar))
assert_equal('protect me', o.inspect)
assert_not_equal(4, o.send(:table!))
# not protected:
assert_equal(5, o.each_pair)
assert_equal('hello', o.to_s)
end end
def test_mistaken_subclass def test_mistaken_subclass