-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from mbta/cbj/errors
feat(OpenTripPlannerClient.Error): more detailed erroring
- Loading branch information
Showing
10 changed files
with
331 additions
and
78 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
defmodule OpenTripPlannerClient.Error do | ||
@moduledoc """ | ||
Describes errors from OpenTripPlanner, including routing errors from the /plan | ||
endpoint for now. For routing errors, generates custom human-friendly error | ||
messages based on routing error code and plan details. | ||
""" | ||
|
||
alias OpenTripPlannerClient.Plan | ||
alias Timex.{Duration, Format.Duration.Formatter} | ||
|
||
require Logger | ||
|
||
defstruct [:details, :message, :type] | ||
|
||
@type t :: %__MODULE__{ | ||
details: map(), | ||
message: String.t(), | ||
type: :graphql_error | :routing_error | ||
} | ||
|
||
@spec from_graphql_error(map()) :: t() | ||
def from_graphql_error(error) do | ||
_ = log_error(error) | ||
|
||
{message, details} = Map.pop(error, :message) | ||
%__MODULE__{details: details, message: message, type: :graphql_error} | ||
end | ||
|
||
@spec from_routing_errors(Plan.t()) :: t() | ||
def from_routing_errors(%Plan{routing_errors: routing_errors} = plan) do | ||
_ = log_error(routing_errors) | ||
|
||
for %{code: code, description: description} <- routing_errors do | ||
%__MODULE__{ | ||
details: %{code: code, description: description}, | ||
message: code_to_message(code, description, plan), | ||
type: :routing_error | ||
} | ||
end | ||
end | ||
|
||
defp code_to_message(code, description, _) | ||
when code in ["LOCATION_NOT_FOUND", "NO_STOPS_IN_RANGE"] do | ||
case description do | ||
"Origin" <> _ -> | ||
"Origin location is not close enough to any transit stops" | ||
|
||
"Destination" <> _ -> | ||
"Destination location is not close enough to any transit stops" | ||
|
||
_ -> | ||
"Location is not close enough to any transit stops" | ||
end | ||
end | ||
|
||
defp code_to_message("NO_TRANSIT_CONNECTION", _, _) do | ||
"No transit connection was found between the origin and destination on this date and time" | ||
end | ||
|
||
defp code_to_message("OUTSIDE_BOUNDS", _, _) do | ||
"Origin or destination location is outside of our service area" | ||
end | ||
|
||
defp code_to_message("NO_TRANSIT_CONNECTION_IN_SEARCH_WINDOW", _, %Plan{} = plan) do | ||
with window when is_binary(window) <- humanized_search_window(plan.search_window_used), | ||
{:ok, formatted_datetime} <- humanized_full_date(plan.date) do | ||
"No transit routes found within #{window} of #{formatted_datetime}. Routes may be available at other times." | ||
else | ||
_ -> | ||
fallback_error_message() | ||
end | ||
end | ||
|
||
defp code_to_message(_, _, _), do: fallback_error_message() | ||
|
||
defp humanized_search_window(number) do | ||
number | ||
|> Duration.from_seconds() | ||
|> Formatter.format(:humanized) | ||
end | ||
|
||
defp humanized_full_date(datetime) when is_integer(datetime) do | ||
datetime | ||
|> OpenTripPlannerClient.Util.to_local_time() | ||
|> Timex.format("{h12}:{m}{am} on {WDfull}, {Mfull} {D}") | ||
end | ||
|
||
defp fallback_error_message do | ||
Application.get_env( | ||
:open_trip_planner_client, | ||
:fallback_error_message | ||
) | ||
end | ||
|
||
defp log_error(errors) when is_list(errors), do: Enum.each(errors, &log_error/1) | ||
|
||
defp log_error(error), do: Logger.error(error) | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
defmodule OpenTripPlannerClient.ErrorTest do | ||
use ExUnit.Case, async: true | ||
|
||
import OpenTripPlannerClient.Error | ||
import OpenTripPlannerClient.Test.Support.Factory | ||
|
||
alias OpenTripPlannerClient.Error | ||
|
||
describe "from_graphql_error/2" do | ||
test "handles one error" do | ||
message = Faker.Lorem.sentence(3, ".") | ||
error = %{message: message} | ||
|
||
assert %Error{details: %{}, type: :graphql_error, message: ^message} = | ||
from_graphql_error(error) | ||
end | ||
end | ||
|
||
describe "from_routing_errors/1" do | ||
test "shows a configured fallback message" do | ||
assert [%Error{message: custom_fallback}] = | ||
from_routing_errors( | ||
build(:plan, routing_errors: [build(:routing_error, %{code: "Fake"})]) | ||
) | ||
|
||
assert custom_fallback == | ||
Application.get_env(:open_trip_planner_client, :fallback_error_message) | ||
end | ||
|
||
test "displays differing message based on error described for origin vs destination" do | ||
plan = | ||
build(:plan, | ||
routing_errors: [ | ||
%{code: "LOCATION_NOT_FOUND", description: "Origin location not found"}, | ||
%{code: "LOCATION_NOT_FOUND", description: "Destination location not found"}, | ||
%{code: "LOCATION_NOT_FOUND", description: "Some other message"} | ||
] | ||
) | ||
|
||
[origin_message, destination_message, message] = | ||
from_routing_errors(plan) |> Enum.map(& &1.message) | ||
|
||
assert origin_message != destination_message | ||
assert origin_message =~ "is not close enough to any transit stops" | ||
assert destination_message =~ "is not close enough to any transit stops" | ||
assert message =~ "Location is not close enough to any transit stops" | ||
end | ||
|
||
test "message for NO_TRANSIT_CONNECTION" do | ||
assert [%Error{message: message}] = | ||
from_routing_errors(plan_with_error_code("NO_TRANSIT_CONNECTION")) | ||
|
||
assert message =~ "No transit connection was found" | ||
end | ||
|
||
test "message for OUTSIDE_BOUNDS" do | ||
assert [%Error{message: message}] = | ||
from_routing_errors(plan_with_error_code("OUTSIDE_BOUNDS")) | ||
|
||
assert message =~ "is outside of our service area" | ||
end | ||
|
||
test "detailed message for NO_TRANSIT_CONNECTION_IN_SEARCH_WINDOW" do | ||
search_window_used = Faker.random_between(600, 7200) | ||
date = Faker.DateTime.forward(2) | ||
|
||
plan = | ||
build(:plan, %{ | ||
date: Timex.to_unix(date) * 1000, | ||
itineraries: [], | ||
routing_errors: [ | ||
build(:routing_error, %{code: "NO_TRANSIT_CONNECTION_IN_SEARCH_WINDOW"}) | ||
], | ||
search_window_used: search_window_used | ||
}) | ||
|
||
assert [%Error{message: message}] = from_routing_errors(plan) | ||
|
||
assert message =~ "Routes may be available at other times" | ||
end | ||
end | ||
|
||
defp plan_with_error_code(code) do | ||
build(:plan, %{ | ||
routing_errors: [build(:routing_error, %{code: code})] | ||
}) | ||
end | ||
end |
Oops, something went wrong.