iagopiimenta/activestorage_legacy

View on GitHub
lib/active_storage/patches/delegation.rb

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
# frozen_string_literal: true

require "set"

class Module
  # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
  # option is not used.
  class DelegationError < NoMethodError; end unless defined?(DelegationError)

  RUBY_RESERVED_KEYWORDS = %w(__ENCODING__ __LINE__ __FILE__ alias and BEGIN begin break
  case class def defined? do else elsif END end ensure false for if in module next nil
  not or redo rescue retry return self super then true undef unless until when while yield) unless defined?(RUBY_RESERVED_KEYWORDS)
  DELEGATION_RESERVED_KEYWORDS = %w(_ arg args block) unless defined?(DELEGATION_RESERVED_KEYWORDS)
  DELEGATION_RESERVED_METHOD_NAMES = Set.new(
    RUBY_RESERVED_KEYWORDS + DELEGATION_RESERVED_KEYWORDS
  ).freeze unless defined?(DELEGATION_RESERVED_METHOD_NAMES)

  # When building decorators, a common pattern may emerge:
  #
  #   class Partition
  #     def initialize(event)
  #       @event = event
  #     end
  #
  #     def person
  #       detail.person || creator
  #     end
  #
  #     private
  #       def respond_to_missing?(name, include_private = false)
  #         @event.respond_to?(name, include_private)
  #       end
  #
  #       def method_missing(method, *args, &block)
  #         @event.send(method, *args, &block)
  #       end
  #   end
  #
  # With <tt>Module#delegate_missing_to</tt>, the above is condensed to:
  #
  #   class Partition
  #     delegate_missing_to :@event
  #
  #     def initialize(event)
  #       @event = event
  #     end
  #
  #     def person
  #       detail.person || creator
  #     end
  #   end
  #
  # The target can be anything callable within the object, e.g. instance
  # variables, methods, constants, etc.
  #
  # The delegated method must be public on the target, otherwise it will
  # raise +DelegationError+. If you wish to instead return +nil+,
  # use the <tt>:allow_nil</tt> option.
  #
  # The <tt>marshal_dump</tt> and <tt>_dump</tt> methods are exempt from
  # delegation due to possible interference when calling
  # <tt>Marshal.dump(object)</tt>, should the delegation target method
  # of <tt>object</tt> add or remove instance variables.
  unless Module.instance_methods(false).include?(:delegate_missing_to)
    def delegate_missing_to(target, allow_nil: nil)
      target = target.to_s
      target = "self.#{target}" if DELEGATION_RESERVED_METHOD_NAMES.include?(target)

      module_eval <<-RUBY, __FILE__, __LINE__ + 1
        def respond_to_missing?(name, include_private = false)
          # It may look like an oversight, but we deliberately do not pass
          # +include_private+, because they do not get delegated.
  
          return false if name == :marshal_dump || name == :_dump
          #{target}.respond_to?(name) || super
        end
  
        def method_missing(method, *args, &block)
          if #{target}.respond_to?(method)
            #{target}.public_send(method, *args, &block)
          else
            begin
              super
            rescue NoMethodError
              if #{target}.nil?
                if #{allow_nil == true}
                  nil
                else
                  raise DelegationError, "\#{method} delegated to #{target}, but #{target} is nil"
                end
              else
                raise
              end
            end
          end
        end
      RUBY
    end
  end
end