lib/cow_proxy/base.rb
module CowProxy
# Base class to create CowProxy classes
#
# Also, it's used as default CowProxy class for non-registered classes
# with copy-on-write disabled, so returned values are wrapped but
# methods trying to change object still raise exception.
class Base
class << self
# Class which will be wrapped with this CowProxy class
attr_accessor :wrapped_class
# Setup wrapped_class and register itself into CowProxy
# with {CowProxy.register_proxy CowProxy.register_proxy}
def inherited(subclass)
subclass.wrapped_class = wrapped_class
CowProxy.register_proxy wrapped_class, subclass if wrapped_class
end
protected
# Return block with proxy implementation.
#
# Block calls a method in wrapped object
#
# @param [Symbol] method Method name to call in wrapped object
# @param [Boolean] cow_enabled True if copy-on-write is enabled
# for proxy class, it will _copy_on_write when method will
# modify wrapped object.
# @return [Proc] Block with proxy implementation.
def wrapping_block(method, cow_enabled)
lambda do |*args, &block|
if method.to_s =~ /^\w+$/
inst_var = "@#{method}"
return _instance_variable_get(inst_var) if _instance_variable_defined?(inst_var)
elsif method.to_s =~ /^(\w+)=$/ && _instance_variable_defined?("@#{Regexp.last_match(1)}")
CowProxy.debug { "remove #{Regexp.last_match(1)}" }
_remove_instance_variable "@#{Regexp.last_match(1)}"
end
__wrapped_method__(inst_var, cow_enabled, method, *args, &block)
end
end
end
# Creates a CowProxy object wrapping obj
#
# @param obj An object to wrap with CowProxy class
# @param parent CowProxy object wrapping obj
# @param parent_var instance variable name in parent
# which keeps this CowProxy object
def initialize(obj, parent = nil, parent_var = nil)
@delegate_dc_obj = obj
@parent_proxy = parent
@parent_var = parent_var
@dc_obj_duplicated = false
end
protected
# Replace wrapped object with a copy, so object can
# be modified.
#
# @param [Boolean] parent Replace proxy object in parent with
# duplicated wrapped object, if this proxy was created from
# another CowProxy.
# @return duplicated wrapped object
def __copy_on_write__(parent = true)
CowProxy.debug { "copy on write on #{__getobj__.class.name}" }
return @delegate_dc_obj if @dc_obj_duplicated
@delegate_dc_obj = @delegate_dc_obj.dup
@dc_obj_duplicated = true
__copy_parent__ if parent && @parent_proxy
@delegate_dc_obj
end
private
def __getobj__
@delegate_dc_obj
end
def __copy_parent__
@parent_proxy.send :__copy_on_write__, false
return unless @parent_var
parent_dc = @parent_proxy._instance_variable_get(:@delegate_dc_obj)
method = @parent_var[1..-1] + '='
parent_dc.send(method, @delegate_dc_obj)
end
def __wrap__(value, inst_var = nil)
return unless value.frozen?
CowProxy.debug { "wrap #{value.class.name} with parent #{__getobj__.class.name}" }
wrap_klass = CowProxy.wrapper_class(value)
wrap_value = wrap_klass&.new(value, self, inst_var)
_instance_variable_set(inst_var, wrap_value) if inst_var && wrap_value
wrap_value
end
def __wrapped_value__(inst_var, method, *args, &block)
CowProxy.debug do
"run on #{__getobj__.class.name} (#{__getobj__.object_id}) "\
"#{method} #{args.inspect unless args.empty?}"
end
value = __getobj__.__send__(method, *args, &block)
wrap_value = __wrap__(value, inst_var) if inst_var && args.empty? && !block
wrap_value || value
end
def __wrapped_method__(inst_var, cow, method, *args, &block)
__wrapped_value__(inst_var, method, *args, &block)
rescue StandardError => e
CowProxy.debug do
"error #{e.message} on #{__getobj__.class.name} (#{__getobj__.object_id}) #{method} "\
"#{args.inspect unless args.empty?} with#{'out' unless cow} cow"
end
raise unless cow && e.message =~ /^can't modify frozen/
CowProxy.debug { "copy on write to run #{method}" }
__copy_on_write__
CowProxy.debug { "new target #{__getobj__.class.name} (#{__getobj__.object_id})" }
__wrapped_value__(inst_var, method, *args, &block)
end
alias _instance_variable_get instance_variable_get
alias _instance_variable_set instance_variable_set
alias _remove_instance_variable remove_instance_variable
alias _instance_variable_defined? instance_variable_defined?
alias _instance_variables instance_variables
end
end