class T::Types::FixedHash

in the list.
Takes a hash of types. Validates each item in a hash using the type in the same position

def build_type

def build_type
  types
  nil
end

def describe_obj(obj)

overrides Base

`Expected {a: String}, got Hash`.
instead of
`Expected {a: String}, got {a: TrueClass}`
This gives us better errors, e.g.:
def describe_obj(obj)
  if obj.is_a?(Hash)
    "type #{serialize_hash(obj.transform_values(&:class))}"
  else
    super
  end
end

def initialize(types)

def initialize(types)
  @inner_types = types
end

def name

overrides Base
def name
  serialize_hash(types)
end

def recursively_valid?(obj)

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

def serialize_hash(hash)

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

def subtype_of_single?(other)

overrides Base
def subtype_of_single?(other)
ther
ixedHash
ing `subtype_of?` here instead of == would be unsound
s == other.types
ypedHash
rning: covariant hashes
, key2, *keys_rest = types.keys.map {|key| T::Utils.coerce(key.class)}
type = if !key2.nil?
:Types::Union::Private::Pool.union_of_types(key1, key2, keys_rest)
f key1.nil?
untyped

y1
e1, value2, *values_rest = types.values
e_type = if !value2.nil?
:Types::Union::Private::Pool.union_of_types(value1, value2, values_rest)
f value1.nil?
untyped

lue1
ypes::TypedHash.new(keys: key_type, values: value_type).subtype_of?(other)
e

def types

def types
  @types ||= @inner_types.transform_values {|v| T::Utils.coerce(v)}
end

def valid?(obj)

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