lib/ash_thrift.ex

defmodule AshThrift do
  defmodule Namespace do
    @type t :: %__MODULE__{
            language: atom(),
            name: String.t()
          }

    defstruct [:language, :name]
  end

  defmodule DslNamespace do
    @type t :: %__MODULE__{
            module: atom()
          }

    defstruct [:module]
  end

  defmodule Field do
    @type t :: %__MODULE__{
            id: non_neg_integer(),
            attribute: atom(),
            optional: boolean(),
            variant: String.t() | nil
          }

    defstruct [:id, :attribute, :optional, :variant]
  end

  defmodule Struct do
    @type t :: %__MODULE__{
            name: String.t(),
            fields: Field.t()
          }

    defstruct [:name, :fields]
  end

  @doc """
  Builds an Ash resource from a thrift struct
  """
  @spec into(
          data :: map(),
          resource :: module(),
          variant :: String.t() | nil,
          dest :: resource_t | nil
        ) :: resource_t
        when resource_t: term()
  def into(data, resource, variant, dest \\ nil)

  def into(data, resource, nil, dest) do
    into(data, resource, relationship_variant(resource, nil), dest)
  end

  def into(data, resource, variant, nil),
    do: into(data, resource, variant, struct(resource))

  def into(data, resource, variant, dest) do
    nil_or_map = case Map.has_key?(dest, :__struct__) do
      true -> nil
      _ -> %{}
    end

    Spark.Dsl.Extension.get_persisted(resource, :thrift, %{})
    |> Map.get(variant, [])
    |> Enum.reduce(dest, fn
      {_field,
       %Ash.Resource.Attribute{
         name: name,
         type: type
       }},
      acc ->
        value = AshThrift.Conversion.parse(type, Map.get(data, name))
        Map.put(acc, name, value)

      {%Field{variant: variant},
       %{
         name: name,
         destination: destination,
         cardinality: cardinality
       }},
      acc ->
        value =
          case {Map.get(data, name), cardinality} do
            {nil, _} ->
              nil

            {data, :one} ->
              into(data, destination, variant, nil_or_map)

            {data, :many} ->
              data
              |> Enum.map(&into(&1, destination, variant, nil_or_map))
          end

        Map.put(acc, name, value)
    end)
  end

  @doc """
  Dumps an Ash resource to a thrift struct
  """
  @spec dump(
          resource :: resource_t,
          variant :: String.t() | nil,
          thrift_struct :: map() | nil
        ) ::
          resource_t
        when resource_t: struct()
  def dump(resource, variant \\ nil, dest \\ nil)

  def dump(resource, nil, dest) do
    dump(resource, relationship_variant(resource.__struct__, nil), dest)
  end

  def dump(resource, variant, nil) do
    dest = struct(variant_module(resource.__struct__, variant))
    dump(resource, variant, dest)
  end

  def dump(resource, variant, dest) do
    Spark.Dsl.Extension.get_persisted(resource.__struct__, :thrift, %{})
    |> Map.get(variant, [])
    |> Enum.reduce(dest, fn
      {_field,
       %Ash.Resource.Attribute{
         name: name,
         type: type
       }},
      acc ->
        value = AshThrift.Conversion.value(type, Map.get(resource, name))
        Map.put(acc, name, value)

      {%Field{variant: variant},
       %{
         name: name,
         cardinality: cardinality
       }},
      acc ->
        value =
          case {Map.get(resource, name), cardinality} do
            {nil, _} ->
              nil

            {%Ash.NotLoaded{}, _} ->
              nil

            {data, :one} ->
              dump(data, variant)

            {data, :many} ->
              data
              |> Enum.map(&dump(&1, variant))
          end

        Map.put(acc, name, value)
    end)
  end

  @spec relationship_variant(resource :: module(), variant :: String.t() | nil) :: String.t()
  def relationship_variant(resource, nil) do
    Spark.Dsl.Extension.get_persisted(resource, :thrift, %{})
    |> Map.keys()
    |> List.first()
  end

  def relationship_variant(_, variant), do: variant

  @spec variant_module(resource :: module(), variant :: String.t() | nil) :: module()
  def variant_module(resource, nil) do
    variant_module(resource, relationship_variant(resource, nil))
  end

  def variant_module(resource, variant) do
    namespace =
      Spark.Dsl.Extension.get_entities(resource, [:thrift])
      |> Enum.filter(fn
        %DslNamespace{} -> true
        _ -> false
      end)
      |> List.first()

    case namespace do
      nil ->
        String.to_existing_atom("Elixir.#{variant}")

      %DslNamespace{module: module} ->
        Module.safe_concat(module, variant)
    end
  end
end