Programatica/cow_proxy

View on GitHub
lib/cow_proxy/base.rb

Summary

Maintainability
A
3 hrs
Test Coverage
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