Skip to content

🏹🚆 Trainsformer Validations: Make sure all errors are reported to frontend#1354

Open
firestack wants to merge 10 commits intomasterfrom
kf/feat/trainsformer-export-error-refactor
Open

🏹🚆 Trainsformer Validations: Make sure all errors are reported to frontend#1354
firestack wants to merge 10 commits intomasterfrom
kf/feat/trainsformer-export-error-refactor

Conversation

@firestack
Copy link
Member

@firestack firestack commented Feb 3, 2026

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.ExportUpload validation module, and how ArrowWeb Trainsformer 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

  • Meets ticket's acceptance criteria
  • Any new or changed functions have typespecs
  • Tests were added for any new functionality (don't just rely on Codecov)
  • This branch was deployed to the staging environment and is currently running with no unexpected increase in warnings, and no errors or crashes.

@firestack firestack marked this pull request as ready for review February 3, 2026 22:27
@firestack firestack requested a review from a team as a code owner February 3, 2026 22:27
@firestack firestack requested review from jzimbel-mbta and removed request for a team February 3, 2026 22:27
@firestack firestack force-pushed the kf/feat/trainsformer-export-error-refactor branch from 94dbfb2 to 9af52e1 Compare February 3, 2026 22:32
@firestack firestack force-pushed the kf/feat/trainsformer-export-error-refactor branch from 9af52e1 to 3e78eef Compare February 3, 2026 22:34
Comment on lines 312 to 319
defp alert_message(%{data: data} = assigns) when not is_nil(data) do
~H"""
{@message}
<ul>
<li :for={element <- @data}>{element}</li>
</ul>
"""
end
Copy link
Member Author

Choose a reason for hiding this comment

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

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 :/

Copy link
Member Author

Choose a reason for hiding this comment

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

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

@firestack firestack force-pushed the kf/feat/trainsformer-export-error-refactor branch from 3a41102 to 704ecfa Compare February 3, 2026 22:57
@firestack firestack force-pushed the kf/feat/trainsformer-export-error-refactor branch from 704ecfa to d54c3bc Compare February 4, 2026 12:51
@firestack firestack force-pushed the kf/feat/trainsformer-export-error-refactor branch from d54c3bc to 621fbc6 Compare February 4, 2026 12:53
Copy link
Member

@jzimbel-mbta jzimbel-mbta left a comment

Choose a reason for hiding this comment

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

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)
Copy link
Member

Choose a reason for hiding this comment

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

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)

Copy link
Member Author

Choose a reason for hiding this comment

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

I also agree with that, I'll go ahead and modify that.

"""
end

defp export_alert(%{error: {type, {key, {_, metadata} = error_contents}}} = assigns) do
Copy link
Member

Choose a reason for hiding this comment

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

suggestion:
Not sure if it's possible for a private function component, but if it is, would you mind documenting the component's attrs?

Copy link
Member Author

Choose a reason for hiding this comment

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

I'll see 👍🏻


defp alert_message(%{key: :stop_id_not_in_gtfs} = assigns) do
~H"""
Some stops are not present in GTFS!
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

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>
Copy link
Member

Choose a reason for hiding this comment

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

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)}.

Copy link
Member Author

Choose a reason for hiding this comment

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

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")
Copy link
Member

Choose a reason for hiding this comment

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

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,
Copy link
Member

Choose a reason for hiding this comment

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

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.

Copy link
Member Author

Choose a reason for hiding this comment

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

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()}}})
Copy link
Member

Choose a reason for hiding this comment

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

question:
Any reason for binary() instead of String.t()?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, nope! good catch!

:both
new_warning(
:invalid_sides,
"Export contains trips serving North and South Station"
Copy link
Member

Choose a reason for hiding this comment

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

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",
Copy link
Member

Choose a reason for hiding this comment

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

nit:
Prefer "service IDs"

Comment on lines +288 to +290
defp alert_message(%{key: :stop_id_not_in_gtfs} = assigns) do
~H"""
Some stops are not present in GTFS!
Copy link
Member

Choose a reason for hiding this comment

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

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.

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.

2 participants