lib/money/ecto/composite_type.ex

if Code.ensure_loaded?(Ecto.Type) do
  defmodule Money.Ecto.Composite.Type do
    @moduledoc """
    Provides a type for Ecto to store a multi-currency price.
    The underlying data type should be an user-defined Postgres composite type `:money_with_currency`.

    ## Migration

        execute "CREATE TYPE public.money_with_currency AS (amount integer, currency varchar(3))"

        create table(:my_table) do
          add :price, :money_with_currency
        end

    ## Schema

        schema "my_table" do
          field :price, Money.Ecto.Composite.Type
        end

    """

    if macro_exported?(Ecto.Type, :__using__, 1) do
      use Ecto.Type
    else
      @behaviour Ecto.Type
    end

    @spec type() :: :money_with_currency
    def type, do: :money_with_currency

    @spec load({integer(), atom() | String.t()}) :: {:ok, Money.t()}
    def load({amount, currency}) do
      {:ok, Money.new(amount, currency)}
    end

    @spec dump(any()) :: :error | {:ok, {integer(), String.t()}}
    def dump(%Money{} = money), do: {:ok, {money.amount, to_string(money.currency)}}
    def dump(_), do: :error

    @spec cast(Money.t() | {integer(), String.t()} | map() | any()) :: :error | {:ok, Money.t()}
    def cast(%Money{} = money) do
      {:ok, money}
    end

    def cast({amount, currency}) when is_integer(amount) and (is_binary(currency) or is_atom(currency)) do
      {:ok, Money.new(amount, currency)}
    end

    def cast(%{"amount" => amount, "currency" => currency})
        when is_integer(amount) and (is_binary(currency) or is_atom(currency)) do
      {:ok, Money.new(amount, currency)}
    end

    def cast(%{amount: amount, currency: currency})
        when is_integer(amount) and (is_binary(currency) or is_atom(currency)) do
      {:ok, Money.new(amount, currency)}
    end

    def cast(_), do: :error
  end
end