🏹🚆 Trainsformer Validations: Make sure all errors are reported to frontend#1354
🏹🚆 Trainsformer Validations: Make sure all errors are reported to frontend#1354
Conversation
94dbfb2 to
9af52e1
Compare
9af52e1 to
3e78eef
Compare
| defp alert_message(%{data: data} = assigns) when not is_nil(data) do | ||
| ~H""" | ||
| {@message} | ||
| <ul> | ||
| <li :for={element <- @data}>{element}</li> | ||
| </ul> | ||
| """ | ||
| end |
There was a problem hiding this comment.
note: one issue I have with this implementation is that there's this undocumented/implicit behavior where if there's a metadata key that matches the key of the error/warning, it uses this implementation to render the contents nicely as a list.
I wanted to use translate_error/1 or some Gettext function to do this, but I haven't been able to figure out how to blend HTML/HEEx with Gettext so this was the best idea I could come up with :/
There was a problem hiding this comment.
I tried to make it a bit more clear with 704ecfa (this PR)
But that doesn't help with discoverability of the "convention" in export_upload.ex
3a41102 to
704ecfa
Compare
704ecfa to
d54c3bc
Compare
…ith list data to generic function
…ement case more clear
d54c3bc to
621fbc6
Compare
jzimbel-mbta
left a comment
There was a problem hiding this comment.
I think this is good overall, though it does add some complexity (or consistent structure, under a more positive light) to the error handling logic.
My comments are mostly on minor things like warning/error message content and small code improvement opportunities.
I made the mistake of commenting along the way as I worked through your changes commit by commit, which resulted in a lot of comments on outdated code where the issue was addressed in a subsequent commit. Took me a bit at the end to clear those out. Oops!
|
|
||
| stops_missing_from_gtfs = | ||
| Enum.filter(trainsformer_stop_ids, fn stop -> !MapSet.member?(gtfs_stop_ids, stop) end) | ||
| Enum.filter(stop_ids, fn stop -> !MapSet.member?(gtfs_stop_ids, stop) end) |
There was a problem hiding this comment.
nit:
Prefer avoiding negated boolean conditions when possible, for clarity.
Also, any reason for using MapSet.member?/2 instead of just in/2?
My suggested change:
Enum.reject(stop_ids, fn stop -> stop in gtfs_stop_ids end)(GH doesn't seem to support making one-click-commit suggestions when working through the PR commit by commit)
There was a problem hiding this comment.
I also agree with that, I'll go ahead and modify that.
| """ | ||
| end | ||
|
|
||
| defp export_alert(%{error: {type, {key, {_, metadata} = error_contents}}} = assigns) do |
There was a problem hiding this comment.
suggestion:
Not sure if it's possible for a private function component, but if it is, would you mind documenting the component's attrs?
|
|
||
| defp alert_message(%{key: :stop_id_not_in_gtfs} = assigns) do | ||
| ~H""" | ||
| Some stops are not present in GTFS! |
There was a problem hiding this comment.
suggestion:
I would have trouble digging up the relevant slack message, but Shanti has previously expressed a preference for no "!"'s in error messages to keep the UX a bit more chill. 😎
Recommend changing this and the other ones to periods.
There was a problem hiding this comment.
Agreed, and I wasn't sure if I should change it, but it goes along with this #1354 (comment), so I'll make it use @message from the validations!
|
|
||
| ~H""" | ||
| <%= if not is_nil(@title) do %> | ||
| <span :if={not is_nil(@title)} class="font-bold">{@title}</span> |
There was a problem hiding this comment.
suggestion:
Since this tag is inside the if not is_nil(@title) check, I think you don't need to check it again--you can remove :if={not is_nil(@title)}.
There was a problem hiding this comment.
Oh whoops, you're right, this was a last second refactor, thanks for catching this!!
| path: "test/support/fixtures/trainsformer/valid_export.zip" | ||
| ) | ||
| |> assert_text("Export contains previously used service ids") | ||
| |> assert_text("Export contains previously used service_id's") |
There was a problem hiding this comment.
nit:
I missed this change in the code, but I think "service ids" or even "service IDs" is more appropriate for error message copy.
service_id is how it's labeled in in code, but that's mainly because we can't have spaces in identifiers.
| # If there is a metadata element with the same name as the alert `key` | ||
| # try to use that as enumerable metadata to report alongside the message | ||
| enum_metadata: | ||
| if(is_atom(key) and Enumerable.impl_for(metadata[key]) != nil, |
There was a problem hiding this comment.
question:
What type(s) of Enumerable besides list can metadata[key] be?
Is there any reason why we can't require a list only, given that in the end it's used by this template code:
<li :for={element <- @enum_metadata}>{element}</li>which I believe would fail if @enum_metadata were any map or keyword list, or any collection that produces values that don't implement the String.Chars protocol when enumerated.
There was a problem hiding this comment.
We could require a list, I figured Enumerable was better to be less restrictive, we do send MapSet's instead of just lists, but I could flatten it at the validation layer.
| missing_routes: [String.t()], | ||
| invalid_routes: [String.t()], | ||
| trips_missing_transfers: MapSet.t() | ||
| warnings: list({:warning, {any(), {binary(), keyword()}}}) |
There was a problem hiding this comment.
question:
Any reason for binary() instead of String.t()?
| :both | ||
| new_warning( | ||
| :invalid_sides, | ||
| "Export contains trips serving North and South Station" |
There was a problem hiding this comment.
suggestion:
Maybe "both North Station and South Station", for clarity?
| if Enum.empty?(existing_service_ids_intersection) do | ||
| :ok | ||
| if Enum.any?(existing_service_ids_intersection) do | ||
| new_error(:existing_service_ids, "Export contains previously used service_id's", |
| defp alert_message(%{key: :stop_id_not_in_gtfs} = assigns) do | ||
| ~H""" | ||
| Some stops are not present in GTFS! |
There was a problem hiding this comment.
question/issue:
I'm a little confused at this point, doesn't this message already get supplied at export_upload.ex:234? Why not render the supplied message instead of writing out a new (and slightly different) one?
This question may also apply to the other alert_message/1 component's clauses that render literal copy based on the error key.
Summary of changes
Asana Ticket: 🏹🚆 Trainsformer Validations: Make sure all errors are reported to frontend
This is a somewhat large refactor of the way we're reporting errors and warnings from the
Arrow.Trainsformer.ExportUploadvalidation module, and howArrowWebTrainsformer pages use those errors & warnings.This change bundles reporting invalid CSV errors and reporting multiple errors and warnings at once because implementing multiple errors without also implementing changes on the other functions was a bit of a mess to manage multiple error "shapes" on top of the existing ways errors were reported.
I tried a few different ways of returning multiple errors & warnings to the frontend, and a few different ways of returning errors (4 element tuple for warnings & errors, rather than nested tuples) and I found that I think this is the implementation I favored the most in terms of friction and extensibility.
I tried to make the commits make logical sense in the order of refactors and changes, so you should be able to read the commits in order to understand the intention and reasoning.
Reviewer Checklist