diff --git a/lib/money/ecto/money_ecto_composite_type.ex b/lib/money/ecto/money_ecto_composite_type.ex index 60b73e7..4cd1c39 100644 --- a/lib/money/ecto/money_ecto_composite_type.ex +++ b/lib/money/ecto/money_ecto_composite_type.ex @@ -189,27 +189,11 @@ if Code.ensure_loaded?(Ecto.Type) do end def cast(%{"currency" => currency, "amount" => amount}, params) - when (is_binary(currency) or is_atom(currency)) and is_integer(amount) do - case Money.new(currency, amount, params) do - %Money{} = money -> {:ok, money} - {:error, {exception, message}} -> {:error, exception: exception, message: message} - end - end - - def cast(%{"currency" => currency, "amount" => amount}, params) - when (is_binary(currency) or is_atom(currency)) and is_binary(amount) do - case Money.new(currency, amount, params) do - %Money{} = money -> {:ok, money} - {:error, {exception, message}} -> {:error, exception: exception, message: message} - end - end - - def cast(%{"currency" => currency, "amount" => %Decimal{} = amount}, params) - when is_binary(currency) or is_atom(currency) do - case Money.new(currency, amount, params) do - %Money{} = money -> {:ok, money} - {:error, {exception, message}} -> {:error, exception: exception, message: message} - end + when (is_binary(currency) or is_atom(currency)) and + (is_integer(amount) or is_binary(amount) or is_struct(amount, Decimal)) do + currency + |> Money.new(amount, params) + |> cast_result() end def cast(%{currency: currency, amount: amount}, params) do @@ -217,16 +201,25 @@ if Code.ensure_loaded?(Ecto.Type) do end def cast(string, params) when is_binary(string) do - case Money.parse(string, params) do - {:error, {exception, message}} -> {:error, exception: exception, message: message} - money -> {:ok, money} - end + string + |> Money.parse(params) + |> cast_result() end def cast(_money, _params) do :error end + # Money returns `{:error, {module, message}}` for its own errors and + # `{:error, exception}` for errors from Localize, such as an invalid locale. + defp cast_result(%Money{} = money), do: {:ok, money} + + defp cast_result({:error, {exception, message}}), + do: {:error, exception: exception, message: message} + + defp cast_result({:error, exception}) when is_exception(exception), + do: {:error, exception: exception.__struct__, message: Exception.message(exception)} + # embed_as is set to :dump because if it is set to # `:self` then `cast/2` will be called when loading. And # since casting is locale-sensitive, the results may diff --git a/test/money_ecto_edge_case_test.exs b/test/money_ecto_edge_case_test.exs index 70124f6..dc25f2d 100644 --- a/test/money_ecto_edge_case_test.exs +++ b/test/money_ecto_edge_case_test.exs @@ -70,6 +70,27 @@ defmodule Money.Ecto.EdgeCaseTest do assert Composite.cast(nil, @params) == {:ok, nil} assert Composite.cast(42, @params) == :error end + + test "an invalid locale is a cast error for \"100 CHF\" and a CHF 100 map" do + params = Composite.init(locale: "!invalid!") + + for type <- [Composite, Map], + input <- [ + "100 CHF", + %{"currency" => "CHF", "amount" => "100"}, + %{currency: :CHF, amount: "100"} + ] do + assert {:error, exception: Localize.InvalidLocaleError, message: message} = + type.cast(input, params) + + assert message =~ "!invalid!" + end + end + + test "an unknown currency XYZ is still a cast error" do + assert {:error, exception: Money.UnknownCurrencyError, message: _} = + Composite.cast(%{"currency" => "XYZ", "amount" => "1"}, @params) + end end describe "composite type equality and embedding" do