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 15, 2024
1 parent 63777f9 commit d47fbd3
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 16 deletions.
55 changes: 40 additions & 15 deletions lib/screens/alerts/cache/filter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ defmodule Screens.Alerts.Cache.Filter do
@moduledoc """
Logic to apply filters to a list of `Screens.Alerts.Alert` structs.
"""
alias Screens.Stops.Stop

@default_activities ~w[BOARD EXIT RIDE]

@type filter_opts() :: %{
Expand All @@ -16,20 +18,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 +53,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 +87,24 @@ 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
{:ok, routes} = stop_mod().fetch_routes_for_stops(values)

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

defp build_matcher({:activities, values}, acc) when is_list(values) do
# activities are filtered later, no need to add matchers
acc
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 +126,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 stop_mod do
Application.get_env(:screens, :alerts_cache_filter_stops_mod, Stop)
end
end
13 changes: 13 additions & 0 deletions lib/screens/stops/stop.ex
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,19 @@ defmodule Screens.Stops.Stop do
end
end

@callback fetch_routes_for_stops(stop_ids :: String.t() | [String.t()]) ::
{:ok, [Route.t()]} | {:error, term()}
def fetch_routes_for_stops(stop_ids) do
with {:ok, %{"included" => route_data}} <-
Screens.V3Api.get_json("route_patterns", %{
"filter[stop]" => stop_ids |> List.wrap() |> Enum.join(","),
"filter[canonical]" => true,
"include" => "route"
}) do
{:ok, Enum.map(route_data, &Screens.Routes.Parser.parse_route/1)}
end
end

# --- END API functions ---

def stop_on_route?(stop_id, stop_sequence) when not is_nil(stop_id) do
Expand Down
17 changes: 16 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.Stops.Stop

defp alert_json(id) do
%{
Expand Down Expand Up @@ -110,6 +113,13 @@ defmodule Screens.Alerts.AlertTest do

describe "fetch_from_cache/2" do
setup do
default_stops_mod = Application.get_env(:screens, :alerts_cache_filter_stops_mod)
Application.put_env(:screens, :alerts_cache_filter_stops_mod, Stop.Mock)

on_exit(fn ->
Application.put_env(:screens, :alerts_cache_filter_stops_mod, default_stops_mod)
end)

alerts = [
%Alert{
id: "USING_WHEELCHAIR",
Expand Down Expand Up @@ -178,11 +188,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(Stop.Mock, :fetch_routes_for_stops, fn _ -> {:ok, []} end)

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

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

import Mox

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

describe "build_matchers/1" do
setup do
default_stops_mod = Application.get_env(:screens, :alerts_cache_filter_stops_mod)
Application.put_env(:screens, :alerts_cache_filter_stops_mod, Stop.Mock)

on_exit(fn ->
Application.put_env(:screens, :alerts_cache_filter_stops_mod, default_stops_mod)
end)
end

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(Stop.Mock, :fetch_routes_for_stops, 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(Stop.Mock, :fetch_routes_for_stops, 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(Stop.Mock, :fetch_routes_for_stops, 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.Stops.Stop.Mock, for: Screens.Stops.Stop)

0 comments on commit d47fbd3

Please sign in to comment.