lib/types/types/fixed_hash.rb
# frozen_string_literal: true # typed: true module T::Types # Takes a hash of types. Validates each item in a hash using the type in the same position # in the list. class FixedHash < Base def initialize(types) @inner_types = types end def types @types ||= @inner_types.transform_values {|v| T::Utils.coerce(v)} end def build_type types nil end # overrides Base def name serialize_hash(types) end # overrides Base def recursively_valid?(obj) return false unless obj.is_a?(Hash) return false if types.any? {|key, type| !type.recursively_valid?(obj[key])} return false if obj.any? {|key, _| !types[key]} true end # overrides Base def valid?(obj) return false unless obj.is_a?(Hash) return false if types.any? {|key, type| !type.valid?(obj[key])} return false if obj.any? {|key, _| !types[key]} true end # overrides Base private def subtype_of_single?(other) case other when FixedHash # Using `subtype_of?` here instead of == would be unsound types == other.types when TypedHash # warning: covariant hashes key1, key2, *keys_rest = types.keys.map {|key| T::Utils.coerce(key.class)} key_type = if !key2.nil? T::Types::Union::Private::Pool.union_of_types(key1, key2, keys_rest) elsif key1.nil? T.untyped else key1 end value1, value2, *values_rest = types.values value_type = if !value2.nil? T::Types::Union::Private::Pool.union_of_types(value1, value2, values_rest) elsif value1.nil? T.untyped else value1 end T::Types::TypedHash.new(keys: key_type, values: value_type).subtype_of?(other) else false end end # This gives us better errors, e.g.: # `Expected {a: String}, got {a: TrueClass}` # instead of # `Expected {a: String}, got Hash`. # # overrides Base def describe_obj(obj) if obj.is_a?(Hash) "type #{serialize_hash(obj.transform_values(&:class))}" else super end end private def serialize_hash(hash) entries = hash.map do |(k, v)| if Symbol === k && ":#{k}" == k.inspect "#{k}: #{v}" else "#{k.inspect} => #{v}" end end "{#{entries.join(', ')}}" end end end