Skip to content

Return a cast error when Money returns an exception - #46

Open
rubas wants to merge 1 commit into
ex-money:mainfrom
rubas:fix/cast-exception-errors
Open

rubas wants to merge 1 commit into
ex-money:mainfrom
rubas:fix/cast-exception-errors

Conversation

@rubas

@rubas rubas commented Sep 26, 2026

Copy link
Copy Markdown

Before

iex> params = Money.Ecto.Composite.Type.init(locale: "!invalid!")
iex> Money.Ecto.Composite.Type.cast("100 CHF", params)
{:ok, {:error, %Localize.InvalidLocaleError{locale_id: "!invalid!"}}}
iex> Money.Ecto.Composite.Type.cast(%{"currency" => "CHF", "amount" => "100"}, params)
** (CaseClauseError) no case clause matching:
    {:error, %Localize.InvalidLocaleError{locale_id: "!invalid!"}}

Problem

  • A string cast succeeds with an error tuple as the value, so the changeset looks valid.
  • A map cast crashes.
  • Money.Ecto.Map.Type delegates cast/2, so it has the same bug.

After

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)}
iex> Money.Ecto.Composite.Type.cast("100 CHF", params)
{:error, [exception: Localize.InvalidLocaleError, message: "The locale \"!invalid!\" is not valid."]}
iex> Money.Ecto.Composite.Type.cast(%{"currency" => "CHF", "amount" => "100"}, params)
{:error, [exception: Localize.InvalidLocaleError, message: "The locale \"!invalid!\" is not valid."]}
Details
  • Cause: Money.Ecto.Composite.Type.cast/2 matches only {:error, {module, message}}. Money.new/3 and Money.parse/2 return {:error, exception} for errors from Localize.
  • Both shapes now go through cast_result/1 and keep the existing exception: and message: keyword error.
  • The three map clauses for an integer, binary, and Decimal amount had the same body, so they are now one clause.
  • Tests: two cases in test/money_ecto_edge_case_test.exs, for both types and for string, string-keyed map, and atom-keyed map input.
  • mix test (179 passed, 5 skipped, PostgreSQL 17) and mix format --check-formatted pass.

AI disclosure: AI models helped us find this issue, write the change, and review it.

Money returns {:error, exception} for Localize errors such as an invalid
locale. Casting a string then returned {:ok, {:error, exception}}, and
casting a map raised CaseClauseError.

Convert both error shapes into Ecto cast errors in one place, and merge the
three identical map clauses.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant