Skip to content

Instantly share code, notes, and snippets.

@estum
Last active January 26, 2023 09:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save estum/288a0ae3f2ae2d45093407790124e86b to your computer and use it in GitHub Desktop.
Save estum/288a0ae3f2ae2d45093407790124e86b to your computer and use it in GitHub Desktop.
Dry::Types::Tuple #dry-rb #dry-types
# frozen_string_literal: true
module Dry
module Types
# @example
# Types::ServiceArgs = Types.Tuple(
# Types::Params::Symbol, # --- positional types
# [Types::Params::Integer | Types::Coercible::String] # --- [type for the rest items]
# )
# Types::ServiceArgs[['thumb', '300', '300', 'sample']]
# # => [:thumb, 300, 300, "sample"]
class Tuple < Array
def initialize(_primitive, types_index:, meta: EMPTY_HASH)
super(_primitive, types_index: types_index, meta: meta)
end
# @return [Hash]
#
# @api public
def types_index
options[:types_index]
end
# @return [Array<Type>]
#
# @api public
def fixed_types
options[:types_index].values
end
# @return [Type]
#
# @api public
def rest_type
options[:types_index].default
end
# @return [String]
#
# @api public
def name
"Tuple"
end
# @param [Array] tuple
#
# @return [Array]
#
# @api private
def call_unsafe(tuple)
try(tuple) { |failure|
raise MapError, failure.error.message
}.input
end
# @param [Array] tuple
#
# @return [Array]
#
# @api private
def call_safe(tuple)
try(tuple) { return yield }.input
end
# @param [Array] tuple
#
# @return [Result]
#
# @api public
def try(tuple)
result = coerce(tuple)
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)
structure = [*fixed_types.map { |type| type.to_ast(meta: true) }]
structure << [rest_type.to_ast(meta: true)] unless rest_type.nil?
structure << meta ? self.meta : EMPTY_HASH
[:tuple, structure]
end
# @return [Boolean]
#
# @api public
def constrained?
rest_type&.constrained? || options[:types_index].each_value.any?(&:contrained?)
end
private
# @api private
def coerce(input)
unless primitive?(input)
return failure(
input, CoercionError.new("#{input.inspect} must be an instance of #{primitive}")
)
end
output = []
failures = []
input.each_with_index do |value, index|
res_i = types_index[index].try(value)
if res_i.failure?
failures << res_i.error
else
output << res_i.input
end
end
if failures.empty?
success(output)
else
failure(input, MultipleError.new(failures))
end
end
end
module BuilderMethods
# Build a tuple type.
#
# @overload Tuple(*fixed_types, rest_type)
# @param [Array<Dry::Types::Type>] fixed_types
# @param [Array(Dry::Types::Type)] rest_type
#
# @return [Dry::Types::Tuple]
def Tuple(*types)
rest_type = Undefined
if types[-1].is_a?(::Array)
if types[-1].size > 1
raise ArgumentError, "rest_type should be an Array with single element to apply to the rest of items"
end
rest_type = types.pop[0]
end
types = ::Array.wrap(types)
types_index = ::Hash[types.size.times.zip(types)]
types_index.default = Undefined.default(rest_type, nil)
Tuple.new(::Array, types_index: types_index)
end
end
# @api private
class Printer
MAPPING[Tuple] = :visit_tuple
def visit_tuple(tuple)
options = tuple.options.dup
size = tuple.fixed_types.size
size += 1 unless tuple.rest_type.nil?
types = options.delete(:types_index)
visit_options(options, tuple.meta) do |opts|
header = "Tuple<#{opts}"
rest = visit(types.default) { |type| "*: #{type}" } if types.default
if size.zero?
yield "#{header}>"
else
yield header.dup << (types.map { |pos, pos_type|
visit(pos_type) { |type| "0: #{type}" }
} << rest).compact.join(", ") << ">"
end
end
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment