lib/type_check/builtin/fixed_tuple.ex

defmodule TypeCheck.Builtin.FixedTuple do
  defstruct [:element_types]

  use TypeCheck
  @type! t :: %__MODULE__{element_types: list(TypeCheck.Type.t())}

  @type! problem_tuple ::
           {t(), :not_a_tuple, %{}, any()}
           | {t(), :different_size, %{expected_size: integer()}, tuple()}
           | {t(), :element_error,
              %{problem: lazy(TypeCheck.TypeError.Formatter.problem_tuple()), index: integer()},
              tuple()}

  defimpl TypeCheck.Protocols.Escape do
    def escape(s) do
      update_in(
        s.element_types,
        &Enum.map(&1, fn val -> TypeCheck.Protocols.Escape.escape(val) end)
      )
    end
  end

  defimpl TypeCheck.Protocols.ToCheck do
    def to_check(s = %{element_types: types_list}, param) do
      element_checks_ast = build_element_checks_ast(types_list, param, s)
      expected_size = length(types_list)

      quote generated: true, location: :keep do
        case unquote(param) do
          x when not is_tuple(x) ->
            {:error, {unquote(Macro.escape(s)), :not_a_tuple, %{}, x}}

          x when tuple_size(x) != unquote(expected_size) ->
            {:error,
             {unquote(TypeCheck.Internals.Escaper.escape(s)), :different_size,
              %{expected_size: unquote(expected_size)}, x}}

          _ ->
            unquote(element_checks_ast)
        end
      end
    end

    defp build_element_checks_ast(types_list, param, s) do
      element_checks =
        types_list
        |> Enum.with_index()
        |> Enum.flat_map(fn {element_type, index} ->
          impl =
            TypeCheck.Protocols.ToCheck.to_check(
              element_type,
              quote generated: true, location: :keep do
                elem(unquote(param), unquote(index))
              end
            )

          quote generated: true, location: :keep do
            [
              {{:ok, element_bindings, altered_element}, _index} <-
                {unquote(impl), unquote(index)},
              bindings = element_bindings ++ bindings,
              altered_param = Tuple.append(altered_param, altered_element)
            ]
          end
        end)

      quote generated: true, location: :keep do
        bindings = []
        altered_param = {}

        with unquote_splicing(element_checks) do
          {:ok, bindings, altered_param}
        else
          {{:error, error}, index} ->
            {:error,
             {unquote(Macro.escape(s)), :element_error, %{problem: error, index: index},
              unquote(param)}}
        end
      end
    end
  end

  defimpl TypeCheck.Protocols.Inspect do
    def inspect(s, opts) do
      element_types =
        case s.element_types do
          %TypeCheck.Builtin.FixedList{element_types: element_types} ->
            element_types

          %TypeCheck.Builtin.List{element_type: element_type} ->
            [element_type]

          other ->
            other
        end

      element_types
      |> List.to_tuple()
      |> Elixir.Inspect.inspect(%Inspect.Opts{
        opts
        | inspect_fun: &TypeCheck.Protocols.Inspect.inspect/2
      })
    end
  end

  if Code.ensure_loaded?(StreamData) do
    defimpl TypeCheck.Protocols.ToStreamData do
      def to_gen(s) do
        s.element_types
        |> Enum.map(&TypeCheck.Protocols.ToStreamData.to_gen/1)
        |> List.to_tuple()
        |> StreamData.tuple()
      end
    end
  end
end