Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Ecto.InvalidChangesetError message for embeds #1796

Merged
merged 5 commits into from Dec 11, 2016
Merged

Improve Ecto.InvalidChangesetError message for embeds #1796

merged 5 commits into from Dec 11, 2016

Conversation

wojtekmach
Copy link
Member

Discussed on ML: https://groups.google.com/forum/#!topic/elixir-ecto/dB8Iu02YQO4

Before:

could not perform insert because changeset is invalid.

* Changeset changes

%{posts: [#Ecto.Changeset<action: :insert, changes: %{}, errors: [title: {"can't be blank", [validation: :required]}], data: #Ecto.ErrorTest.Post<>, valid?: false>, #Ecto.Changeset<action: :insert, changes: %{comments: [#Ecto.Changeset<action: :insert, changes: %{}, errors: [text: {"can't be blank", [validation: :required]}], data: #Ecto.ErrorTest.Comment<>, valid?: false>]}, errors: [title: {"can't be blank", [validation: :required]}], data: #Ecto.ErrorTest.Post<>, valid?: false>]}

* Changeset params

%{"name" => "", "posts" => [%{}, %{"comments" => [%{"text" => ""}], "title" => ""}]}

* Changeset errors

[name: {"can't be blank", [validation: :required]}]

After:

could not perform insert because changeset is invalid.

Changeset changes

    %{posts: [%{comments: [%{}]}]}

Changeset params

    %{"name" => "", "posts" => [%{"comments" => [%{"text" => ""}], "title" => ""}]}

Changeset errors

    %{name: [{"can't be blank", [validation: :required]}], posts: [%{title: [{"can't be blank", [validation: :required]}]}, %{comments: [%{text: [{"can't be blank", [validation: :required]}]}], title: [{"can't be blank", [validation: :required]}]}]}

(see an included temporary test that generates this message)

Note: I've initially used traverse_error/2 to showcase deeply nested changesets. Problem with just using traverse_error that is we'd lose distinction between changeset.errors and changeset.changes.foo.errors which may or may not be a good thing. Alternatively, something close to what was originally proposed on ML could be used, e.g.: (not sure about the exact format)

Changeset errors

    [name: {"can't be blank", [validation: :required]}]
    posts:
      [title: {"can't be blank", [validation: :required]}] # changeset.changes.posts[0]

      [title: {"can't be blank", [validation: :required]}] # changeset.changes.posts[1]
      comments:
        [title: {"can't be blank", [validation: :required]}]

@elixir-bot
Copy link

Hello, @wojtekmach! This is your first Pull Request that will be reviewed by Ebert, an automatic Code Review service. It will leave comments on this diff with potential issues and style violations found in the code as you push new commits. You can also see all the issues found on this Pull Request on its review page. Please check our documentation for more information.

@michalmuskala
Copy link
Member

My only problem with this is that the values we show here are different from the fields in the changeset itself. I worry that this could be confusing. Maybe we should somehow make it more obvious that it's not what is actually stored in the changeset as the data, but rather it's logical value/interpretation.

@ericmj
Copy link
Member

ericmj commented Nov 13, 2016

It's a bit redundant to include "Changeset". What about removing it from each heading like this:

could not perform insert because changeset is invalid.

Changes

    %{posts: [%{comments: [%{}]}]}

Params

    %{"name" => "", "posts" => [%{"comments" => [%{"text" => ""}], "title" => ""}]}

Errors

    %{name: [{"can't be blank", [validation: :required]}], posts: [%{title: [{"can't be blank", [validation: :required]}]}, %{comments: [%{text: [{"can't be blank", [validation: :required]}]}], title: [{"can't be blank", [validation: :required]}]}]}

This way I think it will also be less associated with the exact changeset fields. Do you think that helps @michalmuskala?

@wojtekmach
Copy link
Member Author

Another solution is to include the Changeset in the exception message (although we rarely if ever do that in other exceptions) although that might be a bit too redundant:

preview

@josevalim
Copy link
Member

@michalmuskala @wojtekmach how should we proceed here then? :)

@wojtekmach
Copy link
Member Author

@michalmuskala ping :) I like @ericmj proposal the most, I also provided an alternative (that would be my 2nd choice)

@michalmuskala
Copy link
Member

I think I like the solution with printing the whole changeset too. It might be helpful in some occasions to see the whole, unprocessed data, that we show currently.

@ericmj
Copy link
Member

ericmj commented Nov 21, 2016

It might be helpful in some occasions to see the whole, unprocessed data, that we show currently.

In those cases it is easy to inspect the changeset. I think in the majority of cases the changeset is not interesting.

@michalmuskala
Copy link
Member

Yeah, that's true. I'm still worried that printing something under Changes: that is different from what is in changeset.changes will be confusing. I'm not sure how to make it "not confusing".

@josevalim
Copy link
Member

@michalmuskala that's a good point. The changeset may also have important information, such as which action is going to be used. Maybe what we could do to improve this is by adding width: 80 when inspecting the changesets so at least we don't have a huge garbled message?

@wojtekmach
Copy link
Member Author

wojtekmach commented Nov 25, 2016

@josevalim @ericmj @michalmuskala wdyt about the following? (I've added a commit) The top level changeset.action was always displayed in the first sentence of the error, but for embedded changesets I can see how it could be useful to see the actions at the bottom:

zrzut ekranu 2016-11-25 o 10 11 29

@fishcakez
Copy link
Member

fishcakez commented Nov 25, 2016 via email

@wojtekmach
Copy link
Member Author

wojtekmach commented Nov 30, 2016

I tried that but it looks like it's not converting \n in exception message, and the changeset (without pretty printing) suffers from the same "garbled message" problem.

(thanks for the tip about :erlang.error/2 though, I did not know about it! :))

zrzut ekranu 2016-12-01 o 00 31 33

@fishcakez
Copy link
Member

fishcakez commented Nov 30, 2016

@wojtekmach I meant to call it with the InvalidChangesetError exception as first argument, not the message. Like

:erlang.error(Ecto.InvalidChangesetError.exception(...), [changeset])

@wojtekmach
Copy link
Member Author

wojtekmach commented Nov 30, 2016

ugh, doh. let me double check.

(edit) @fishcakez thanks, I've updated the screenshot above. Personally, I find the changeset being part of exception message (and be pretty printed) to be more readable

@wojtekmach
Copy link
Member Author

hey everyone, just checking-in (sorry for nagging!): we clearly have a few different ways to display this as shown in the screenshots above, is there one way we could all agree on? Personally I think I like the 2nd screenshot from the top the most (pretty printing data and changeset), but I think any of the ones shown above would be an improvement over the original message.

@ericmj
Copy link
Member

ericmj commented Dec 9, 2016

I agree with @wojtekmach, I prefer the 2nd screenshot.

@josevalim
Copy link
Member

josevalim commented Dec 9, 2016

I am ok with the second screenshot as long as we call the first entry "Applied changes" or something similar.

|> Enum.map_join("\n", &" " <> &1)
end

defp extract_changes(%Ecto.Changeset{changes: changes}) do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the changeset is marked as action: :delete, should we maybe remove it from showing?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josevalim do you mean the "main" changeset is marked as delete or one of the embeds changesets? If the main changeset is marked as delete, do you mean that we wouldn't show the last part:

     ** (Ecto.InvalidChangesetError) could not perform delete because changeset is invalid.

     Applied changes

         %{posts: [nil, %{}, %{title: "new"}, %{title: nil}, %{title: "new name"}]}

     Params

         %{"posts" => [%{"title" => "new"}, %{"id" => "2", "title" => nil},
            %{"id" => "3", "title" => "new name"}]}

     Errors

         %{posts: [%{}, %{}, %{},
            %{title: [{"can't be blank", [validation: :required]}]}, %{}]}

     (Changeset used to be here)

or hide "Applied changes" section or something else?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that, when we hide the changeset, we can no longer know if the changeset is going to be inserted/updated/deleted. This way, if something was meant to be deleted but we still include it, it can be confusing. So I thought we should hide the changes not for the main changeset but any of the children changeset that is marked as delete.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@josevalim okay thanks for clarifying it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in ebdf13c

@michalmuskala
Copy link
Member

I'm fine with going with any of the proposed solutions. They are all better from what we have right now.

end)
end
defp extract_changes([%Ecto.Changeset{action: :delete} | tail]),
do: [extract_changes(tail)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be wrapped in a list.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch, fixed

@josevalim josevalim merged commit 4877374 into elixir-ecto:master Dec 11, 2016
@josevalim
Copy link
Member

❤️ 💚 💙 💛 💜

@wojtekmach wojtekmach deleted the wm-invalid-changeset-error-improvements branch December 12, 2016 10:31
bartekupartek pushed a commit to bartekupartek/ecto that referenced this pull request Mar 19, 2019
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.

None yet

6 participants