diff --git a/lib/screens/alerts/cache/filter.ex b/lib/screens/alerts/cache/filter.ex index 6a5cbff56..a121e7a10 100644 --- a/lib/screens/alerts/cache/filter.ex +++ b/lib/screens/alerts/cache/filter.ex @@ -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() :: %{ @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/lib/screens/stops/stop.ex b/lib/screens/stops/stop.ex index e8d258d43..6a2e1ea7c 100644 --- a/lib/screens/stops/stop.ex +++ b/lib/screens/stops/stop.ex @@ -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 diff --git a/test/screens/alerts/alert_test.exs b/test/screens/alerts/alert_test.exs index ac77a98db..5efb6df4c 100644 --- a/test/screens/alerts/alert_test.exs +++ b/test/screens/alerts/alert_test.exs @@ -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 %{ @@ -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", @@ -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) diff --git a/test/screens/alerts/cache/filter_test.exs b/test/screens/alerts/cache/filter_test.exs new file mode 100644 index 000000000..aedccf33a --- /dev/null +++ b/test/screens/alerts/cache/filter_test.exs @@ -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 diff --git a/test/test_helper.exs b/test/test_helper.exs index 869559e70..6eead0061 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1,3 @@ ExUnit.start() + +Mox.defmock(Screens.Stops.Stop.Mock, for: Screens.Stops.Stop)