# frozen_string_literal: true require "dry/core/equalizer" require "dry/types/fn_container" require "dry/types/constructor/function" require "dry/types/constructor/wrapper" module Dry module Types # Constructor types apply a function to the input that is supposed to return # a new value. Coercion is a common use case for constructor types. # # @api public class Constructor < Nominal include Dry::Equalizer(:type, :options, inspect: false, immutable: true) # @return [#call] attr_reader :fn # @return [Type] attr_reader :type undef :constrained?, :meta, :optional?, :primitive, :default?, :name # @param [Builder, Object] input # @param [Hash] options # @param [#call, nil] block # # @api public def self.new(input, **options, &block) type = input.is_a?(Builder) ? input : Nominal.new(input) super(type, **options, fn: Function[options.fetch(:fn, block)]) end # @param [Builder, Object] input # @param [Hash] options # @param [#call, nil] block # # @api public def self.[](type, fn:, **options) function = Function[fn] if function.wrapper? wrapper_type.new(type, fn: function, **options) else new(type, fn: function, **options) end end # @api private def self.wrapper_type @wrapper_type ||= begin if self < Wrapper self else const_set(:Wrapping, ::Class.new(self).include(Wrapper)) end end end # Instantiate a new constructor type instance # # @param [Type] type # @param [Function] fn # @param [Hash] options # # @api private def initialize(type, fn: nil, **options) @type = type @fn = fn super(type, **options, fn: fn) end # @return [Object] # # @api private def call_safe(input) coerced = fn.(input) { |output = input| return yield(output) } type.call_safe(coerced) { |output = coerced| yield(output) } end # @return [Object] # # @api private def call_unsafe(input) type.call_unsafe(fn.(input)) end # @param [Object] input # @param [#call,nil] block # # @return [Logic::Result, Types::Result] # @return [Object] if block given and try fails # # @api public def try(input, &block) value = fn.(input) rescue CoercionError => e failure = failure(input, e) block_given? ? yield(failure) : failure else type.try(value, &block) end # Build a new constructor by appending a block to the coercion function # # @param [#call, nil] new_fn # @param [Hash] options # @param [#call, nil] block # # @return [Constructor] # # @api public def constructor(new_fn = nil, **options, &block) next_fn = Function[new_fn || block] if next_fn.wrapper? self.class.wrapper_type.new(with(**options), fn: next_fn) else with(**options, fn: fn >> next_fn) end end alias_method :append, :constructor alias_method :>>, :constructor # @return [Class] # # @api private def constrained_type Constrained::Coercible end # @see Nominal#to_ast # # @api public def to_ast(meta: true) [:constructor, [type.to_ast(meta: meta), fn.to_ast]] end # Build a new constructor by prepending a block to the coercion function # # @param [#call, nil] new_fn # @param [Hash] options # @param [#call, nil] block # # @return [Constructor] # # @api public def prepend(new_fn = nil, **options, &block) with(**options, fn: fn << (new_fn || block)) end alias_method :<<, :prepend # Build a lax type # # @return [Lax] # @api public def lax Lax.new(constructor_type[type.lax, **options]) end # Wrap the type with a proc # # @return [Proc] # # @api public def to_proc proc { |value| self.(value) } end private # @param [Symbol] meth # @param [Boolean] include_private # @return [Boolean] # # @api private def respond_to_missing?(meth, include_private = false) super || type.respond_to?(meth) end # Delegates missing methods to {#type} # # @param [Symbol] method # @param [Array] args # @param [#call, nil] block # # @api private def method_missing(method, *args, &block) if type.respond_to?(method) response = type.public_send(method, *args, &block) if response.is_a?(Type) && type.class.equal?(response.class) response.constructor_type[response, **options] else response end else super end end end end end