dry-types/lib/dry/types/map.rb

133 lines
3.1 KiB
Ruby

# frozen_string_literal: true
module Dry
module Types
# Homogeneous mapping. It describes a hash with unknown keys that match a certain type.
#
# @example
# type = Dry::Types['hash'].map(
# Dry::Types['integer'].constrained(gteq: 1, lteq: 10),
# Dry::Types['string']
# )
#
# type.(1 => 'right')
# # => {1 => 'right'}
#
# type.('1' => 'wrong')
# # Dry::Types::MapError: "1" violates constraints (type?(Integer, "1") AND gteq?(1, "1") AND lteq?(10, "1") failed)
#
# type.(11 => 'wrong')
# # Dry::Types::MapError: 11 violates constraints (lteq?(10, 11) failed)
#
# @api public
class Map < Nominal
def initialize(_primitive, key_type: Types['any'], value_type: Types['any'], meta: EMPTY_HASH)
super(_primitive, key_type: key_type, value_type: value_type, meta: meta)
end
# @return [Type]
#
# @api public
def key_type
options[:key_type]
end
# @return [Type]
#
# @api public
def value_type
options[:value_type]
end
# @return [String]
#
# @api public
def name
'Map'
end
# @param [Hash] hash
#
# @return [Hash]
#
# @api private
def call_unsafe(hash)
try(hash) { |failure|
raise MapError, failure.error.message
}.input
end
# @param [Hash] hash
#
# @return [Hash]
#
# @api private
def call_safe(hash)
try(hash) { return yield }.input
end
# @param [Hash] hash
#
# @return [Result]
#
# @api public
def try(hash)
result = coerce(hash)
return result if result.success? || !block_given?
yield(result)
end
# @param meta [Boolean] Whether to dump the meta to the AST
#
# @return [Array] An AST representation
#
# @api public
def to_ast(meta: true)
[:map,
[key_type.to_ast(meta: true),
value_type.to_ast(meta: true),
meta ? self.meta : EMPTY_HASH]]
end
# @return [Boolean]
#
# @api public
def constrained?
value_type.constrained?
end
private
# @api private
def coerce(input)
return failure(
input, CoercionError.new("#{input.inspect} must be an instance of #{primitive}")
) unless primitive?(input)
output, failures = {}, []
input.each do |k, v|
res_k = key_type.try(k)
res_v = value_type.try(v)
if res_k.failure?
failures << res_k.error
elsif output.key?(res_k.input)
failures << CoercionError.new("duplicate coerced hash key #{res_k.input.inspect}")
elsif res_v.failure?
failures << res_v.error
else
output[res_k.input] = res_v.input
end
end
if failures.empty?
success(output)
else
failure(input, MultipleError.new(failures))
end
end
end
end
end