-
Notifications
You must be signed in to change notification settings - Fork 13
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
feat: Create a data structure to populate the system status widget #2330
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could probably be cleaned up a bit. One suggestion would be to write comments for all of the private functions. I find that it helps add clarity when I have to write narrative descriptions.
9e706d9
to
0a31d30
Compare
a12cad9
to
42f225d
Compare
0a31d30
to
58b5500
Compare
58b5500
to
c7e9dee
Compare
%{ | ||
statuses: [%{time: nil, description: "Normal Service"}], | ||
sub_routes: [], | ||
route_id: ^route_id | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion(non-blocking): This could read real nice broken out into a function, like Enum.all?(lines, &shows_normal_service?/1)
which validates the nil time and "Normal Service" text (though I suppose you'd want a separate check to verify the correct route IDs involved, maybe)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Question: I've given the tests a pretty thorough once-over (actually more like a twice- or thrice- over). Do you think this is still necessary?
lib/dotcom/system_status/groups.ex
Outdated
# &groups/2 returns an ordered data structure, sorted in the order | ||
# given by `@lines`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment should be in a @doc
above the def
line
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should restructure this comment to make it clearer that it's actually about @lines
, not groups/2
lib/dotcom/system_status/groups.ex
Outdated
@routes ["Blue", "Mattapan", "Orange", "Red"] ++ @green_line_branches | ||
|
||
def groups(alerts, time) do | ||
grouped_alerts = Map.new(@routes, &{&1, alerts_for_line(alerts, &1)}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That function should be alerts_for_route
if it operates per-route, no?
I also wonder if something in the Alerts.Match
module might meet your needs, what if Alerts.Match.match(alerts, %InformedEntity{route: &1})
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That function should be
alerts_for_route
if it operates per-route, no?
Yep! Done.
I also wonder if something in the
Alerts.Match
module might meet your needs, what ifAlerts.Match.match(alerts, %InformedEntity{route: &1})
?
Ugh - I agree, but I've been struggling to actually get Alerts.Match
to work, and I'm not sure exactly why. I still think it's worth using the machinery that already exists, but would you mind if I did that as a follow-up instead?
Has the following features: - Groups alerts into their line - Stringifies the time to a human-readable time, nil, or "Now", depending on context - Sorts statuses within a line in a reasonable way
…mbiguously named "status"
7a900f9
to
675cd71
Compare
(and rename the naive alerts_to_statuses to alerts_to_statuses_naive)
Pulled back into draft because I realized that the data structure isn't quite the right shape to accommodate the desired frontend |
…d designs This is a huge change, but there wasn't really a good way to break this up into smaller commits
…atuses_for_line/3
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, I don't like the implicit
use of nil as a time in order to try to simplify logic. It makes for using functions that purportedly take a time, but don't really. You could utilize module attributes a lot more to hardcode a lot less.
defp alert(opts) do | ||
route_id = opts |> Keyword.fetch!(:route_id) | ||
effect = opts[:effect] || Faker.Util.pick(@effects) | ||
active_period = opts |> Keyword.fetch!(:active_period) | ||
|
||
Alert.build(:alert, | ||
effect: effect, | ||
informed_entity: | ||
InformedEntitySet.build(:informed_entity_set, | ||
route: route_id, | ||
entities: [ | ||
InformedEntity.build(:informed_entity, route: route_id) | ||
] | ||
), | ||
active_period: active_period | ||
) | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any code that goes toward building an alert should go in the alerts factory.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I sort of see that, although exactly what belongs in a factory versus just here eludes me at the moment.
The problem is that I don't want to have to copy-pasta the informed_entity
/ informed_entity_set
nesting over and over again, but I'm not sure that it makes sense to pull this into a factory as-is either.
There probably is a way to do it gracefully, but unless we can get that taken care of real fast, I think I'll ask that we tackle it as a follow-up, since this work is blocking the rest of the project.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can see in this factory: https://github.com/mbta/dotcom/pull/2336/files#diff-8fbc124c966696c04346223e24b3cc686bb63d07f0f3a35316e93f8dd30c6384 how I separated _factory
functions from other functions.
@all_rail_lines ["Blue", "Green", "Orange", "Red"] | ||
@green_line_branches ["Green-B", "Green-C", "Green-D", "Green-E"] | ||
@heavy_rail_lines ["Blue", "Orange", "Red"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can export these out of the module.
defmodule Dotcom.SystemStatus.Groups do
@heavy_rail_lines ~w[Blue Orange Red]
@all_rail_lines @heavy_rails_lines ++ ~w[Green]
def heavy_rail_lines, do: @heavy_rail_lines
def all_rail_lines, do: @all_rail_lines
end
defmodule Dotcom.SystemStatus.GroupsTest do
describe "groups/2" do
test "sorts groups by heavy rail first" do
# Exercise
groups = Group.groups([], time_today())
# Verify
route_ids = groups |> Enum.map(&1.route_id)
heavy_rail_lines = heavy_rail_lines()
assert Enum.take(route_ids, Kernel.length(heavy_rail_lines)) == heavy_rail_lines
end
end
end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see that that's possible, but what is the benefit?
My problem is that it makes the tests a bit more tautological - instead of the assertion coming from the tests, the assertion comes from a mix of the source code and the tests, which means that the tests don't validate a user-facing behavior so much as validating that the code is interacting with its own internal constants.
I especially feel that way about this suggestion because the constants you mentioned aren't present in the source file, because I didn't need them. I would be exporting an internal constant that the source file doesn't even need for the sake of the tests, which is the kind of refactor that I've usually considered an antipattern.
I'll think on it for a little longer, but that's my first impression.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You don't necessarily have to have the heavy rail lines in a module attribute, though I think you did before. If you're getting them from somewhere else then you can get them there in the test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like you're just using @lines
now. So, you can export that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
...though I think you did before.
This is exactly why I don't want to export the module attributes. I did a pretty hefty refactor to Groups.groups/2
that changed which module attributes existed, and that refactor would have been harder if I had been exporting them. They're implementation details, and they change during refactors.
I see that that's possible, but what is the benefit?
Bump on this question, which kind of feels like it makes all the difference.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The benefit is that the test is more communicative.
|
||
# Translates an alert to a status: | ||
# - The effect is humanized into a description for the status. | ||
# - If the alert's already active, `time` is set to `nil`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not clear on why setting the time to nil means that it is active.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way this will be rendered in the frontend will be something like:
case time do
nil -> description
_ -> "#{time}: #{description}"
end
Thinking about other ways of representing this.... maybe prefix
instead of time
, since in a certain sense, that's actually what it is /slash/ how it will be used?
# If there are no alerts, then we want a single status indicating | ||
# "Normal Service". | ||
defp alerts_to_statuses_naive([], _time) do | ||
[%{description: "Normal Service", time: nil}] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there ever a case where the description is "Normal Service" and the time is not nil? Also, I see "Normal Service" in multiple places in this module and in the tests. Why not make it a module attribute and export it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there ever a case where the description is "Normal Service" and the time is not nil?
I don't think so.
Also, I see "Normal Service" in multiple places in this module and in the tests. Why not make it a module attribute and export it?
Touché, although I'm not keen on exporting it for the same reason that I mentioned here.
case alert.effect do | ||
:delay -> "Delays" | ||
:shuttle -> "Shuttle Buses" | ||
:station_closure -> "Station Closure" | ||
:suspension -> "Suspension" | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a module attribute of a map or a function using pattern matching. What happens if the alert.effect isn't in this list?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be a module attribute of a map or a function using pattern matching.
True. This felt pretty much equivalent to both of those 🤷♂️.
What happens if the alert.effect isn't in this list?
If the alert effect isn't in this list, that's an error that I'd probably want to see in Sentry. How do we typically handle this kind of thing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Usually pattern matching and the fallback logs the error with the argument.
# Returns true if the active period ends before the time given. An | ||
# end-time of false indicates an indefinite active period, which | ||
# never ends. | ||
defp ends_before?({_start_time, nil}, _time), do: false | ||
defp ends_before?({_start_time, end_time}, time), do: Timex.before?(end_time, time) | ||
|
||
# Returns true if the active period starts before the time given. | ||
defp starts_before?({start_time, _end_time}, time), do: Timex.before?(start_time, time) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure we need these since they just wrap Timex functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ends_before?
is needed, but maybe starts_before?
isn't. It looked a bit odd to me to extract one and not the other, but I guess I can inline starts_before?
.
defp stringify_time(nil), do: nil | ||
defp stringify_time(time), do: Util.kitchen_downcase_time(time) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't like the fact that stringify_time takes something that isn't a time and returns something that isn't a string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you have a better name in mind? maybe_time_to_prefix
(assuming this suggestion)?
@@ -0,0 +1,465 @@ | |||
defmodule Dotcom.SystemStatus.Groups do | |||
@moduledoc """ | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't have these spaces in the moduledoc of any other modules.
|
||
Returns a data structure that can be used in the system status |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't use this empty line in docs anywhere else. We shouldn't describe this module as being for use in front-end component. It should be useful on its own. Something like takes alerts groups them in order to communicate system status of each route
.
:suspension -> "Suspension" | ||
end | ||
|
||
time = future_start_time(alert.active_period, time) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
question: Does Alerts.Match.any_time_match?/2
do the same thing?
Summary of changes
Asana Ticket: System Status Flowchart | Implement the bulk of the flowchart
Parent Ticket: System Status | Add module that implements the system-status flowchart
Depends on: