jirutka/corefines

View on GitHub
lib/corefines/object.rb

Summary

Maintainability
A
2 hrs
Test Coverage
require 'corefines/support/alias_submodules'
require 'set'

module Corefines
  module Object
    ##
    # @!method blank?
    #   An object is blank if it's +false+, empty, or a whitespace string.
    #   For example, <tt>'', '   ', "\t\n\r", "\u00a0", nil, [], {}</tt> are
    #   all blank.
    #
    #   This simplifies
    #
    #     address.nil? || address.empty?
    #
    #   to
    #
    #     address.blank?
    #
    #   @return [Boolean]
    #
    # @!method presence
    #   Returns object if it's not {#blank?}, otherwise returns +nil+.
    #   +obj.presence+ is equivalent to <tt>obj.blank? ? nil : obj</tt>.
    #
    #   This is handy for any representation of objects where blank is the same
    #   as not present at all. For example, this simplifies a common check for
    #   HTTP POST/query parameters:
    #
    #     state = params[:state] if params[:state].present?
    #     country = params[:country] if params[:country].present?
    #     region = state || country || 'CZ'
    #
    #   becomes...
    #
    #     region = params[:state].presence || params[:country].presence || 'CZ'
    #
    #   @return [Object, nil] object if it's not {#blank?}, otherwise +nil+.
    #
    module Blank

      BLANK_RE = /\A[[:space:]]*\z/

      refine ::Object do
        def blank?
          respond_to?(:empty?) ? !!empty? : !self
        end

        def presence
          self unless blank?
        end
      end

      refine ::NilClass do
        def blank?
          true
        end
      end

      refine ::FalseClass do
        def blank?
          true
        end
      end

      refine ::TrueClass do
        def blank?
          false
        end
      end

      refine ::Array do
        alias_method :blank?, :empty?
      end

      refine ::Hash do
        alias_method :blank?, :empty?
      end

      refine ::Numeric do
        def blank?
          false
        end
      end

      refine ::String do
        def blank?
          BLANK_RE === self
        end
      end
    end

    ##
    # @!method deep_dup
    #   Returns a deep copy of itself.
    #
    #   If +self+ is an instance of +Array+, +Set+ or +Hash+, then it
    #   recursively copies its elements as well. If a nested object responds to
    #   +deep_dup+ (note: +respond_to?+ doesn't see refined methods), then it
    #   use it.
    #
    #   @return [Object] a copy of +self+, or +self+ if it's not duplicable.
    #
    module DeepDup
      refine ::Object do
        # NOTE: Refinement is not active inside itself, so we can't simply call
        # deep_dup on a nested object like ActiveSupport does. Thus we must use
        # this ugly approach instead...
        def deep_dup(obj = self)
          return obj.deep_dup if obj != self && obj.respond_to?(:deep_dup)

          case obj
          when ::NilClass, ::FalseClass, ::TrueClass, ::Symbol, ::Numeric, ::Method
            obj  # not duplicable
          when ::Array
            obj.map { |o| deep_dup(o) }
          when ::Set
            ::Set[obj.map { |o| deep_dup(o) }]
          when ::Hash
            obj.each_with_object({}) { |(k, v), h| h[deep_dup(k)] = deep_dup(v) }
          else
            begin
              obj.dup
            rescue TypeError, NoMethodError
              obj
            end
          end
        end
      end
    end

    ##
    # @!method else
    #   Returns +self+ if +self+ evaluates to +true+, otherwise returns the
    #   evaluation of the block.
    #
    #   @yield [self] gives +self+ to the block.
    #   @return [Object] +self+ if +self+ evaluates to +true+, otherwise
    #     returns the evaluation of the block.
    #
    module Else
      refine ::Object do
        def else
          self ? self : yield(self)
        end
      end
    end

    ##
    # @!method in?(other)
    #   @example Array
    #     characters = ["Konata", "Kagami", "Tsukasa"]
    #     "Konata".in?(characters) # => true
    #
    #   @example String
    #     "f".in?("flynn") # => true
    #     "x".in?("flynn") # => false
    #
    #   @param other [#include?]
    #   @return [Boolean] +true+ if this object is included in the _other_
    #     object, +false+ otherwise.
    #   @raise ArgumentError if the _other_ doesn't respond to +#include?+.
    #
    module In
      refine ::Object do
        def in?(other)
          other.include? self
        rescue NoMethodError
          fail ArgumentError, "The parameter passed to #in? must respond to #include?"
        end
      end
    end

    ##
    # @!method instance_values
    #   @example
    #     class C
    #       def initialize(x, y)
    #         @x, @y = x, y
    #       end
    #     end
    #
    #     C.new(0, 1).instance_values
    #     => {x: 0, y: 1}
    #
    #   @return [Hash] a hash with symbol keys that maps instance variable
    #     names without "@" to their corresponding values.
    #
    module InstanceValues
      refine ::Object do
        def instance_values
          ary = instance_variables.map do |name|
            [ name[1..-1].to_sym, instance_variable_get(name) ]
          end
          ::Hash[ary]
        end
      end
    end

    ##
    # @!method then
    #   Returns +self+ if +self+ evaluate to +false+, otherwise returns
    #   evaluation of the block.
    #
    #   This simplifies something like:
    #
    #     if m = "Flynn <flynn@encom.com>".match(/<([^>]+)>/)
    #       m[1]
    #     end
    #
    #   to:
    #
    #     "Flynn <flynn@encom.com>".match(/<([^>]+)>/).then { |m| m[1] }
    #
    #
    #   Since +then+ passes +self+ to the block, it can be also used for
    #   chaining, so something like:
    #
    #     html = parse_html(input)
    #     html = find_nodes(html, "//section")
    #     html = remove_nodes(html, "//p")
    #
    #   can be rewritten to:
    #
    #     parse_html(input)
    #       .then { |h| find_nodes(h, "//section") }
    #       .then { |h| remove_nodes(h, "//p") }
    #
    #   @yield [self] gives +self+ to the block.
    #   @return [Object] evaluation of the block, or +self+ if no block given
    #     or +self+ evaluates to false.
    #
    module Then
      refine ::Object do
        def then
          if block_given? && self
            yield self
          else
            self
          end
        end
      end
    end

    ##
    # @!method then_if(*conditions)
    #   Returns +self+ if +self+ or any of the _conditions_ evaluates to
    #   +false+, otherwise returns the evaluation of the block.
    #
    #   @example
    #     "foo".then_if(:empty?) { "N/A" } # => "foo"
    #        "".then_if(:empty?) { "N/A" } # => "N/A"
    #
    #   Each condition may be of the type:
    #   * +Symbol+ - name of the method to be invoked using +public_send+.
    #   * +Array+  - name of the method followed by arguments to be invoked
    #     using +public_send+.
    #   * +Proc+   - proc to be called with +self+ as the argument.
    #   * Any other object to be evaluated as +true+, or +false+.
    #
    #   @param *conditions conditions to evaluate.
    #   @yield [self] gives +self+ to the block.
    #   @return [Object] evaluation of the block, or +self+ if any condition
    #     evaluates to +false+, or no condition given and +self+ evaluates to
    #     +false+.
    #
    module ThenIf
      refine ::Object do
        def then_if(*conditions)
          return self if conditions.empty? && !self
          return self unless conditions.all? do |arg|
            case arg
            when ::Symbol then public_send(arg)
            when ::Array then public_send(*arg)
            when ::Proc then arg.call(self)
            else arg
            end
          end
          yield self
        end
      end
    end

    ##
    # @!method try(method, *args, &block)
    #   Invokes the public method identified by the symbol _method_, passing it
    #   any arguments and/or the block specified, just like the regular Ruby
    #   +public_send+ does.
    #
    #   *Unlike* that method however, a +NoMethodError+ exception will *not* be
    #   be raised and +nil+ will be returned instead, if the receiving object
    #   doesn't respond to the _method_.
    #
    #   This method is defined to be able to write:
    #
    #     @person.try(:name)
    #
    #   instead of:
    #
    #     @person.name if @person
    #
    #   +try+ calls can be chained:
    #
    #     @person.try(:spouse).try(:name)
    #
    #   instead of:
    #
    #     @person.spouse.name if @person && @person.spouse
    #
    #   +try+ will also return +nil+ if the receiver does not respond to the
    #   method:
    #
    #     @person.try(:unknown_method) # => nil
    #
    #   instead of:
    #
    #     @person.unknown_method if @person.respond_to?(:unknown_method) # => nil
    #
    #   +try+ returns +nil+ when called on +nil+ regardless of whether it
    #   responds to the method:
    #
    #     nil.try(:to_i) # => nil, rather than 0
    #
    #   Arguments and blocks are forwarded to the method if invoked:
    #
    #     @posts.try(:each_slice, 2) do |a, b|
    #       ...
    #     end
    #
    #   The number of arguments in the signature must match. If the object
    #   responds to the method, the call is attempted and +ArgumentError+ is
    #   still raised in case of argument mismatch.
    #
    #   Please also note that +try+ is defined on +Object+. Therefore, it won't
    #   work with instances of classes that do not have +Object+ among their
    #   ancestors, like direct subclasses of +BasicObject+. For example, using
    #   +try+ with +SimpleDelegator+ will delegate +try+ to the target instead
    #   of calling it on the delegator itself.
    #
    #   @param method [Symbol] name of the method to invoke.
    #   @param args arguments to pass to the _method_.
    #   @return [Object, nil] result of calling the _method_, or +nil+ if
    #     doesn't respond to it.
    #
    # @!method try!(method, *args, &block)
    #   Same as {#try}, but raises a +NoMethodError+ exception if the receiver
    #   is not +nil+ and does not implement the tried method.
    #
    #   @example
    #     "a".try!(:upcase) # => "A"
    #     nil.try!(:upcase) # => nil
    #     123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Fixnum
    #
    #   @param method (see #try)
    #   @param args (see #try)
    #   @return (see #try)
    #
    module Try
      refine ::Object do
        def try(method = nil, *args, &block)
          try!(method, *args, &block) if respond_to? method
        end

        def try!(method = nil, *args, &block)
          public_send method, *args, &block
        end
      end

      refine ::NilClass do
        def try(*args)
          nil
        end

        def try!(*args)
          nil
        end
      end
    end

    include Support::AliasSubmodules

    class << self
      alias_method :blank?, :blank
      alias_method :in?, :in
      alias_method :presence, :blank
      alias_method :try!, :try
    end
  end
end