dry-types/lib/dry/types/constructor/function.rb

202 lines
5.3 KiB
Ruby

# frozen_string_literal: true
require 'concurrent/map'
module Dry
module Types
class Constructor < Nominal
# Function is used internally by Constructor types
#
# @api private
class Function
# Wrapper for unsafe coercion functions
#
# @api private
class Safe < Function
def call(input, &block)
@fn.(input, &block)
rescue NoMethodError, TypeError, ArgumentError => error
CoercionError.handle(error, &block)
end
end
# Coercion via a method call on a known object
#
# @api private
class MethodCall < Function
@cache = ::Concurrent::Map.new
# Choose or build the base class
#
# @return [Function]
def self.call_class(method, public, safe)
@cache.fetch_or_store([method, public, safe].hash) do
if public
::Class.new(PublicCall) do
include PublicCall.call_interface(method, safe)
end
elsif safe
PrivateCall
else
PrivateSafeCall
end
end
end
# Coercion with a publicly accessible method call
#
# @api private
class PublicCall < MethodCall
@interfaces = ::Concurrent::Map.new
# Choose or build the interface
#
# @return [::Module]
def self.call_interface(method, safe)
@interfaces.fetch_or_store([method, safe].hash) do
::Module.new do
if safe
module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
def call(input, &block)
@target.#{method}(input, &block)
end
RUBY
else
module_eval(<<~RUBY, __FILE__, __LINE__ + 1)
def call(input, &block)
@target.#{method}(input)
rescue NoMethodError, TypeError, ArgumentError => error
CoercionError.handle(error, &block)
end
RUBY
end
end
end
end
end
# Coercion via a private method call
#
# @api private
class PrivateCall < MethodCall
def call(input, &block)
@target.send(@name, input, &block)
end
end
# Coercion via an unsafe private method call
#
# @api private
class PrivateSafeCall < PrivateCall
def call(input, &block)
@target.send(@name, input)
rescue NoMethodError, TypeError, ArgumentError => error
CoercionError.handle(error, &block)
end
end
# @api private
#
# @return [MethodCall]
def self.[](fn, safe)
public = fn.receiver.respond_to?(fn.name)
MethodCall.call_class(fn.name, public, safe).new(fn)
end
attr_reader :target, :name
def initialize(fn)
super
@target = fn.receiver
@name = fn.name
end
def to_ast
[:method, target, name]
end
end
# Choose or build specialized invokation code for a callable
#
# @param [#call] fn
# @return [Function]
def self.[](fn)
raise ArgumentError, 'Missing constructor block' if fn.nil?
if fn.is_a?(Function)
fn
elsif fn.is_a?(::Method)
MethodCall[fn, yields_block?(fn)]
elsif yields_block?(fn)
new(fn)
else
Safe.new(fn)
end
end
# @return [Boolean]
def self.yields_block?(fn)
*, (last_arg,) =
if fn.respond_to?(:parameters)
fn.parameters
else
fn.method(:call).parameters
end
last_arg.equal?(:block)
end
include Dry::Equalizer(:fn)
attr_reader :fn
def initialize(fn)
@fn = fn
end
# @return [Object]
def call(input, &block)
@fn.(input, &block)
end
alias_method :[], :call
# @return [Array]
def to_ast
if fn.is_a?(::Proc)
[:id, Dry::Types::FnContainer.register(fn)]
else
[:callable, fn]
end
end
if RUBY_VERSION >= '2.6'
# @return [Function]
def >>(other)
proc = other.is_a?(::Proc) ? other : other.fn
Function[@fn >> proc]
end
# @return [Function]
def <<(other)
proc = other.is_a?(::Proc) ? other : other.fn
Function[@fn << proc]
end
else
# @return [Function]
def >>(other)
proc = other.is_a?(::Proc) ? other : other.fn
Function[-> x { proc[@fn[x]] }]
end
# @return [Function]
def <<(other)
proc = other.is_a?(::Proc) ? other : other.fn
Function[-> x { @fn[proc[x]] }]
end
end
end
end
end
end