Generating constraint error messages

ecto
phoenix
errors

#1

Hi everyone!

I have a classic scenario when I can’t delete a Client when they have Projects assigned to them. For the check I have a no_assoc_constraint(:projects) on them and it generates the correct error, but the question is:

What is idiomatic way of turning this changeset errors into a message for the alert?

The changeset.errors outputs:

[projects: {"are still associated with this entry", []}]

I think I want it something like “Projects are still associated with this entry”.

  • Aleksey

#2

From what I’ve done you can either iterate on changeset.errors to build your formatted string or use the message option when setting the constraint: no_assoc_constraint(:projects, message: "Projects are still associated with this entry").


#3

Yep, that’s one way of doing it. I was wondering if there’s anything in the same line with full_messages in Rails. Something that converts the names of fields and error messages into strings using Gettext maybe.


#4

Ended up with something like this:

defmodule MyAppWeb.Errors do

  # Returns the list of full-text messages concatenating the names of fields
  # and error messages taken from the changeset errors.
  #
  # Field names are translated using Gettext's "schema" domain. The key is
  # formed from the name of the data structure (MyApp.MyContext.MyModel,
  # for example: MyApp.Accounts.User) and the name of the field the error
  # is related too (for `email` it becomes `MyApp.Accounts.User.email`
  def full_messages(%Ecto.Changeset{} = changeset) do
    module_name =
      changeset.data.__struct__
      |> Module.split
      |> Enum.join(".")

    changeset.errors
    |> Enum.map(fn {key, error} ->
      key_path = "#{module_name}.#{key}"
      key_name = case Gettext.dgettext(MyAppWeb.Gettext, "schema", key_path) do
        ^key_path -> key
        n -> n
      end

      "#{key_name} #{MyAppWeb.ErrorHelpers.translate_error(error)}"
    end)
  end

end

That I’m calling like this:

msg =
  MyAppWeb.Errors.full_messages(changeset)
  |> Enum.join(" ")

Field names can be customized in standard Gettext schema.pot domain file. Structure is like this: MyApp.MyContext.MyModel.field_name.


#5

It might be easier for you if you use the Changeset.traverse_errors function?


#6

Yes, traverse_errors/2 is actually recommended because it correctly handles association and embed errors (they’re stored in changeset.changes.some_assoc.errors, not in changeset.errors)


#7

Thanks, gents. Something like this?

defmodule MyAppWeb.Errors do

  def full_messages(%Ecto.Changeset{} = changeset) do
    changeset
    |> Ecto.Changeset.traverse_errors(&full_message/3)
    |> Enum.reduce([], fn {_key, errors}, acc -> acc ++ errors end)
  end

  defp full_message(%Ecto.Changeset{} = changeset, key, error) do
    module_name =
      changeset.data.__struct__
      |> Module.split
      |> Enum.join(".")

    key_path = "#{module_name}.#{key}"
    key_name = case Gettext.dgettext(MyAppWeb.Gettext, "schema", key_path) do
      ^key_path -> key
      n -> n
    end

    "#{key_name} #{MyAppWeb.ErrorHelpers.translate_error(error)}"
  end
end


#8

I’d do:

|> Enum.flat_map(&elem(&1, 1))

Just:

module_name = inspect(changeset.data.__struct__)

#9

Thanks, noted.


#10

Using Phoenix.Naming to humanize when there isn’t a translation available is also good idea in case the symbol includes underscores. E.g, :verify_email => Verify email

^key_path -> Phoenix.Naming.humanize(key)

#11

Good one. Thanks!