Skip to content

Commit

Permalink
feat: Expand stops filters to include routes at stops
Browse files Browse the repository at this point in the history
Adds `fetch_routes_for_stops/1` and uses that to expand a `stops:`
filter similar to the V3 API.
  • Loading branch information
sloanelybutsurely committed Aug 16, 2024
1 parent 26910dc commit d116146
Show file tree
Hide file tree
Showing 6 changed files with 116 additions and 17 deletions.
1 change: 1 addition & 0 deletions config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ config :screens,
blue_bikes_station_information_url: [:no_api_requests_allowed_during_testing],
blue_bikes_station_status_url: [:no_api_requests_allowed_during_testing],
blue_bikes_api_client: Screens.BlueBikes.FakeClient,
alerts_cache_filter_routes_mod: Screens.Routes.Route.Mock,
dup_headsign_replacements: %{
"Test 1" => "T1"
},
Expand Down
61 changes: 46 additions & 15 deletions lib/screens/alerts/cache/filter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule Screens.Alerts.Cache.Filter do
@moduledoc """
Logic to apply filters to a list of `Screens.Alerts.Alert` structs.
"""

@default_activities ~w[BOARD EXIT RIDE]

@type filter_opts() :: %{
Expand All @@ -16,20 +17,22 @@ defmodule Screens.Alerts.Cache.Filter do
def filter_by(alerts, filter_opts) when filter_opts == %{}, do: alerts

def filter_by(alerts, filter_opts) do
filter_opts = Map.put_new(filter_opts, :activities, @default_activities)
{activities, filter_opts} = Map.pop(filter_opts, :activities, @default_activities)

alerts
|> filter(filter_opts)
|> filter_by_informed_entity_activity(filter_opts)
|> filter_by_informed_entity_activity(activities)
end

defp filter(alerts, filter_opts) when filter_opts == %{}, do: alerts

defp filter(alerts, filter_opts) do
filter_opts
|> build_matchers()
|> apply_matchers(alerts)
end

defp filter_by_informed_entity_activity(alerts, %{activities: values}) do
defp filter_by_informed_entity_activity(alerts, values) do
values = MapSet.new(values)

if MapSet.member?(values, "ALL") do
Expand All @@ -49,15 +52,20 @@ defmodule Screens.Alerts.Cache.Filter do
end
end

defp filter_by_informed_entity_activity(alerts, filter_opts) do
filter_opts = Map.put(filter_opts, :activities, @default_activities)

filter_by_informed_entity_activity(alerts, filter_opts)
end

defp build_matchers(filter_opts) do
def build_matchers(filter_opts) do
filter_opts
|> Enum.reduce([%{}], &build_matcher/2)
|> reject_empty_matchers()
|> Enum.uniq()
end

defp reject_empty_matchers(matchers) do
matchers
|> Enum.reject(fn matcher ->
matcher
|> Map.values()
|> Enum.all?(&is_nil/1)
end)
end

defp apply_matchers(matchers, alerts) do
Expand All @@ -78,12 +86,31 @@ defmodule Screens.Alerts.Cache.Filter do
end

defp build_matcher({:stops, values}, acc) when is_list(values) do
matchers_for_values(acc, :stop, values)
end
route_mod = route_mod()

defp build_matcher({:activities, values}, acc) when is_list(values) do
# activities are filtered later, no need to add matchers
acc
routes =
values
|> Enum.flat_map(fn stop_id ->
{:ok, routes} = route_mod.serving_stop(stop_id)
routes
end)

route_matchers =
for %{id: route_id} <- routes,
stop_id <- [nil | values] do
%{route: route_id, stop: stop_id}
end

stop_matchers =
for stop_id <- [nil | values] do
%{stop: stop_id}
end

for matcher_list <- [route_matchers, stop_matchers],
merge <- matcher_list,
matcher <- acc do
Map.merge(matcher, merge)
end
end

defp matchers_for_values(acc, key, values) do
Expand All @@ -105,4 +132,8 @@ defmodule Screens.Alerts.Cache.Filter do
defp matches?(alert, {key, value}) do
Enum.any?(alert.informed_entities, &(Map.get(&1, key) == value))
end

defp route_mod do
Application.get_env(:screens, :alerts_cache_filter_routes_mod, Route)
end
end
2 changes: 1 addition & 1 deletion lib/screens/routes/route.ex
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ defmodule Screens.Routes.Route do
end

@doc "Fetches routes that serve the given stop."
@spec serving_stop(Stop.id()) :: {:ok, [t()]} | :error
@callback serving_stop(Stop.id()) :: {:ok, [t()]} | :error
def serving_stop(
stop_id,
get_json_fn \\ &V3Api.get_json/2,
Expand Down
10 changes: 9 additions & 1 deletion test/screens/alerts/alert_test.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
defmodule Screens.Alerts.AlertTest do
use ExUnit.Case, async: true

import Mox

alias Screens.Alerts.Alert
alias Screens.Routes.Route

defp alert_json(id) do
%{
Expand Down Expand Up @@ -178,11 +181,16 @@ defmodule Screens.Alerts.AlertTest do
[alerts: alerts, get_all_alerts: fn -> alerts end]
end

test "returns all of the alerts", %{alerts: alerts, get_all_alerts: get_all_alerts} do
test "returns all of the alerts matching the default activities", %{
alerts: alerts,
get_all_alerts: get_all_alerts
} do
assert {:ok, alerts} == Alert.fetch_from_cache([], get_all_alerts)
end

test "filters by stops", %{get_all_alerts: get_all_alerts} do
stub(Route.Mock, :serving_stop, fn _ -> {:ok, []} end)

assert {:ok, [%Alert{id: "stop: A" <> _}]} =
Alert.fetch_from_cache([stop_id: "A"], get_all_alerts)

Expand Down
57 changes: 57 additions & 0 deletions test/screens/alerts/cache/filter_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
defmodule Screens.Alerts.Cache.FilterTest do
use ExUnit.Case, async: true

import Mox

alias Screens.Alerts.Cache.Filter
alias Screens.Routes.Route

describe "build_matchers/1" do
test "passes through empty filters" do
assert [] == Filter.build_matchers(%{})
end

test "adds matchers for direction_id" do
assert [%{direction_id: 0}] == Filter.build_matchers(%{direction_id: 0})
assert [%{direction_id: 1}] == Filter.build_matchers(%{direction_id: 1})
end

test "adds matchers for route_types" do
assert [%{route_type: 1}, %{route_type: 2}] == Filter.build_matchers(%{route_types: [1, 2]})
end

test "adds matchers for stops" do
stub(Route.Mock, :serving_stop, fn _ -> {:ok, []} end)

assert [
%{stop: "place-pktrm"},
%{stop: "place-aport"}
] = Filter.build_matchers(%{stops: ["place-pktrm", "place-aport"]})
end

test "merges stop matchers into other matchers" do
stub(Route.Mock, :serving_stop, fn _ -> {:ok, []} end)

assert [
%{stop: nil, direction_id: 0},
%{stop: "place-pktrm", direction_id: 0},
%{stop: "place-aport", direction_id: 0}
] == Filter.build_matchers(%{direction_id: 0, stops: ["place-pktrm", "place-aport"]})
end

test "adds matchers for routes at the stops" do
stub(Route.Mock, :serving_stop, fn
"place-aport" ->
{:ok, [%Route{id: "Blue"}, %Route{id: "743"}]}
end)

assert [
%{stop: nil, route: "Blue"},
%{stop: "place-aport", route: "Blue"},
%{stop: nil, route: "743"},
%{stop: "place-aport", route: "743"},
%{stop: "place-aport"}
] == Filter.build_matchers(%{stops: ["place-aport"]})
end
end
end
2 changes: 2 additions & 0 deletions test/test_helper.exs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
ExUnit.start()

Mox.defmock(Screens.Routes.Route.Mock, for: Screens.Routes.Route)

0 comments on commit d116146

Please sign in to comment.