Skip to main content

lib/dsxir/signature/transformer.ex

defmodule Dsxir.Signature.Transformer do
  @moduledoc false

  use Spark.Dsl.Transformer

  alias Spark.Dsl.Transformer

  @doc false
  @spec transform(Spark.Dsl.t()) :: {:ok, Spark.Dsl.t()} | {:error, term()}
  def transform(dsl_state) do
    module = Transformer.get_persisted(dsl_state, :module)
    fields = Transformer.get_entities(dsl_state, [:signature])

    with :ok <- ensure_unique(module, fields, :input),
         :ok <- ensure_unique(module, fields, :output) do
      expand_fields(module, dsl_state, fields)
    end
  end

  defp expand_fields(module, dsl_state, fields) do
    Enum.reduce_while(fields, {:ok, dsl_state}, fn field, {:ok, acc} ->
      case expand_type(field.type) do
        {:ok, zoi} ->
          updated = %{field | zoi: zoi}

          {:cont,
           {:ok, Transformer.replace_entity(acc, [:signature], updated, &same_field?(&1, field))}}

        {:error, reason} ->
          {:halt,
           {:error,
            %Dsxir.Errors.Invalid.Signature{
              module: module,
              field: field.name,
              reason: reason
            }}}
      end
    end)
  end

  defp ensure_unique(module, fields, kind) do
    names =
      fields
      |> Enum.filter(&(&1.kind == kind))
      |> Enum.map(& &1.name)

    case names -- Enum.uniq(names) do
      [] ->
        :ok

      [dup | _] ->
        {:error,
         %Dsxir.Errors.Invalid.Signature{
           module: module,
           field: dup,
           reason: {:duplicate_field, kind}
         }}
    end
  end

  defp same_field?(a, b), do: a.name == b.name and a.kind == b.kind

  defp expand_type(:string), do: {:ok, Zoi.string()}
  defp expand_type(:integer), do: {:ok, Zoi.integer()}
  defp expand_type(:float), do: {:ok, Zoi.float()}
  defp expand_type(:boolean), do: {:ok, Zoi.boolean()}

  defp expand_type({:list, inner}) do
    with {:ok, expanded} <- expand_type(inner) do
      {:ok, Zoi.list(expanded)}
    end
  end

  defp expand_type(%_{} = zoi_struct) do
    if zoi_schema?(zoi_struct),
      do: {:ok, zoi_struct},
      else: {:error, {:not_a_zoi_schema, zoi_struct}}
  end

  defp expand_type(Dsxir.Primitives.History), do: {:ok, Zoi.any()}

  defp expand_type(other), do: {:error, {:unknown_type, other}}

  defp zoi_schema?(struct), do: Zoi.Type.impl_for(struct) != nil
end