Skip to content

Commit

Permalink
Merge branch 'main' into kb-search
Browse files Browse the repository at this point in the history
  • Loading branch information
KaylaBrady authored Jan 23, 2024
2 parents 0b352df + eedfd47 commit f1c401c
Show file tree
Hide file tree
Showing 35 changed files with 1,241 additions and 87 deletions.
3 changes: 3 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ config :logger, :console,
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# Use Req for making HTTP requests
config :mobile_app_backend, MobileAppBackend.HTTP, Req

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{config_env()}.exs"
28 changes: 16 additions & 12 deletions lib/mbta_v3_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ defmodule MBTAV3API do
require Logger
alias MBTAV3API.JsonApi

@spec get_json(String.t(), Keyword.t()) :: JsonApi.t() | {:error, any}
def get_json(url, params \\ [], opts \\ []) do
@type params :: %{String.t() => String.t()}

@spec get_json(String.t(), params(), Keyword.t()) :: JsonApi.t() | {:error, any}
def get_json(url, params \\ %{}, opts \\ []) do
_ =
Logger.debug(fn ->
"MBTAV3API.get_json url=#{url} params=#{params |> Map.new() |> Jason.encode!()}"
"MBTAV3API.get_json url=#{url} params=#{params |> Jason.encode!()}"
end)

body = ""
Expand Down Expand Up @@ -39,7 +41,8 @@ defmodule MBTAV3API do

{time, response} =
:timer.tc(fn ->
Req.get(
Req.new(
method: :get,
base_url: base_url,
url: URI.encode(url),
headers: headers,
Expand All @@ -50,12 +53,13 @@ defmodule MBTAV3API do
pool_timeout: timeout,
receive_timeout: timeout
)
|> MobileAppBackend.HTTP.request()
end)

{time, response}
end

@spec maybe_log_parse_error(JsonApi.t() | {:error, any}, String.t(), Keyword.t(), String.t()) ::
@spec maybe_log_parse_error(JsonApi.t() | {:error, any}, String.t(), params(), String.t()) ::
JsonApi.t() | {:error, any}
defp maybe_log_parse_error({:error, error}, url, params, body) do
_ = log_response_error(url, params, body)
Expand All @@ -66,11 +70,11 @@ defmodule MBTAV3API do
response
end

@spec log_response(String.t(), Keyword.t(), integer, any) :: :ok
@spec log_response(String.t(), params(), integer, any) :: :ok
defp log_response(url, params, time, response) do
entry = fn ->
"MBTAV3API.get_json_response url=#{inspect(url)} " <>
"params=#{params |> Map.new() |> Jason.encode!()} " <>
"params=#{params |> Jason.encode!()} " <>
log_body(response) <>
" duration=#{time / 1000}" <>
" request_id=#{Logger.metadata() |> Keyword.get(:request_id)}"
Expand All @@ -80,19 +84,19 @@ defmodule MBTAV3API do
:ok
end

@spec log_response_error(String.t(), Keyword.t(), String.t()) :: :ok
@spec log_response_error(String.t(), params(), String.t()) :: :ok
defp log_response_error(url, params, body) do
entry = fn ->
"MBTAV3API.get_json_response url=#{inspect(url)} " <>
"params=#{params |> Map.new() |> Jason.encode!()} response=" <> body
"params=#{params |> Jason.encode!()} response=" <> body
end

_ = Logger.info(entry)
:ok
end

defp log_body({:ok, response}) do
"status=#{response.status_code} content_length=#{byte_size(response.body)}"
"status=#{response.status} content_length=#{byte_size(response.body)}"
end

defp log_body({:error, error}) do
Expand All @@ -101,8 +105,8 @@ defmodule MBTAV3API do

defp default_options do
[
base_url: Application.fetch_env!(:mobile_app_backend, :base_url),
api_key: Application.fetch_env!(:mobile_app_backend, :api_key),
base_url: Application.get_env(:mobile_app_backend, :base_url),
api_key: Application.get_env(:mobile_app_backend, :api_key),
timeout: 10_000
]
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mbta_v3_api/headers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ defmodule MBTAV3API.Headers do
defp api_key_header(headers, nil), do: headers

defp api_key_header(headers, <<key::binary>>) do
api_version = Application.get_env(:mbta_v3_api, :api_version)
api_version = Application.get_env(:mobile_app_backend, :api_version)
[{"x-api-key", key}, {"MBTA-Version", api_version} | headers]
end
end
21 changes: 18 additions & 3 deletions lib/mbta_v3_api/json_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ defmodule MBTAV3API.JsonApi.Item do
}
end

defmodule MBTAV3API.JsonApi.Reference do
@moduledoc """
A JSON:API "resource identifier object", with no attribute information.
"""

@derive Jason.Encoder
defstruct [:type, :id]

@type t :: %__MODULE__{
type: String.t(),
id: String.t()
}
end

defmodule MBTAV3API.JsonApi.Error do
@moduledoc """
JSON API error data.
Expand All @@ -40,7 +54,7 @@ defmodule MBTAV3API.JsonApi do

@type t :: %__MODULE__{
links: %{String.t() => String.t()},
data: list(MBTAV3API.JsonApi.Item.t())
data: list(MBTAV3API.JsonApi.Item.t() | MBTAV3API.JsonApi.Reference.t())
}

@spec empty() :: t()
Expand Down Expand Up @@ -87,7 +101,8 @@ defmodule MBTAV3API.JsonApi do
%{}
end

@spec parse_data(term()) :: {:ok, [MBTAV3API.JsonApi.Item.t()]} | {:error, any}
@spec parse_data(term()) ::
{:ok, [MBTAV3API.JsonApi.Item.t() | MBTAV3API.JsonApi.Reference.t()]} | {:error, any}
defp parse_data(%{"data" => data} = parsed) when is_list(data) do
included = parse_included(parsed)
{:ok, Enum.map(data, &parse_data_item(&1, included))}
Expand Down Expand Up @@ -126,7 +141,7 @@ defmodule MBTAV3API.JsonApi do
end

def parse_data_item(%{"type" => type, "id" => id}, _) do
%MBTAV3API.JsonApi.Item{
%MBTAV3API.JsonApi.Reference{
type: type,
id: id
}
Expand Down
19 changes: 19 additions & 0 deletions lib/mbta_v3_api/json_api/filter_value.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
defprotocol MBTAV3API.JsonApi.FilterValue do
@doc "Converts this filter value into a string"
@spec filter_value_string(t()) :: String.t()
def filter_value_string(data)
end

defimpl MBTAV3API.JsonApi.FilterValue, for: BitString do
def filter_value_string(data), do: data
end

defimpl MBTAV3API.JsonApi.FilterValue, for: [Atom, Float, Integer] do
def filter_value_string(data), do: String.Chars.to_string(data)
end

defimpl MBTAV3API.JsonApi.FilterValue, for: List do
def filter_value_string(data) do
Enum.map_join(data, ",", &MBTAV3API.JsonApi.FilterValue.filter_value_string/1)
end
end
20 changes: 20 additions & 0 deletions lib/mbta_v3_api/json_api/object.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
defmodule MBTAV3API.JsonApi.Object do
alias MBTAV3API.JsonApi

@callback fields :: [atom()]
@callback includes :: %{atom() => atom()}

@spec module_for(atom() | String.t()) :: module()
def module_for(type)

for type <- [:route, :route_pattern, :stop, :trip] do
module = :"#{MBTAV3API}.#{Macro.camelize(to_string(type))}"

def module_for(unquote(type)), do: unquote(module)
def module_for(unquote("#{type}")), do: unquote(module)
end

@spec parse(JsonApi.Item.t() | JsonApi.Reference.t()) :: struct() | JsonApi.Reference.t()
def parse(%JsonApi.Item{type: type} = item), do: module_for(type).parse(item)
def parse(%JsonApi.Reference{} = ref), do: ref
end
116 changes: 116 additions & 0 deletions lib/mbta_v3_api/json_api/params.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
defmodule MBTAV3API.JsonApi.Params do
alias MBTAV3API.JsonApi.FilterValue
alias MBTAV3API.JsonApi.Object

@type sort_param :: {atom(), :asc | :desc}
@type fields_param :: {atom(), list(atom())}
@type include_param :: atom() | {atom(), include_param()} | list(include_param())
@type filter_param :: {atom(), FilterValue.t()}
@type param ::
{:sort, sort_param()}
| {:fields, [fields_param()]}
| {:include, include_param()}
| {:filter, [filter_param()]}
@type t :: [param()]

@doc """
Turns a legible parameter list into a flat HTTP-ready parameter list.
## Examples
iex> MBTAV3API.JsonApi.Params.flatten_params(
...> [
...> sort: {:name, :asc},
...> fields: [route: [:color, :short_name]],
...> include: :route,
...> filter: [route: ["Green-B", "Red"], canonical: true]
...> ],
...> :route_pattern
...> )
%{
"sort" => "name",
"fields[route]" => "color,short_name",
"fields[route_pattern]" => "direction_id,name,sort_order",
"include" => "route",
"filter[route]" => "Green-B,Red",
"filter[canonical]" => "true"
}
"""
@spec flatten_params(t(), atom()) :: %{String.t() => String.t()}
def flatten_params(params, root_type) do
Map.merge(
Map.merge(
sort(params[:sort]),
fields(root_type, params[:include], Keyword.get(params, :fields, []))
),
Map.merge(include(params[:include]), filter(params[:filter]))
)
end

@spec sort(nil | sort_param()) :: %{String.t() => String.t()}
defp sort(nil), do: %{}

defp sort({field, dir}) do
%{
"sort" =>
case dir do
:asc -> ""
:desc -> "-"
end <> "#{field}"
}
end

@spec fields(atom(), nil | include_param(), [fields_param()]) :: %{String.t() => String.t()}
defp fields(root_type, include, overrides) do
included_types(root_type, include)
|> Enum.uniq()
|> Enum.map(&{&1, Object.module_for(&1).fields()})
|> Keyword.merge(overrides)
|> Map.new(fn {type, fields} -> {"fields[#{type}]", Enum.join(fields, ",")} end)
end

defp included_types(root_type, nil), do: [root_type]

defp included_types(root_type, include) when is_atom(include) do
[root_type, Map.fetch!(Object.module_for(root_type).includes(), include)]
end

defp included_types(root_type, {include, sub_include}) do
[^root_type, included] = included_types(root_type, include)
[root_type, included] ++ included_types(included, sub_include)
end

defp included_types(root_type, includes) when is_list(includes) do
Enum.flat_map(includes, &included_types(root_type, &1))
end

@spec include(nil | include_param()) :: %{String.t() => String.t()}
defp include(nil), do: %{}

defp include(include) do
%{"include" => flat_include(include) |> Enum.map_join(",", &Enum.join(&1, "."))}
end

@spec flat_include(nil | include_param()) :: Enumerable.t(list(atom()))
defp flat_include(include) when is_atom(include) do
[[include]]
end

defp flat_include({include, sub_include}) do
Stream.concat([[include]], flat_include(sub_include) |> Stream.map(&[include | &1]))
end

defp flat_include(includes) when is_list(includes) do
Stream.flat_map(includes, &flat_include/1)
end

@spec filter(nil | [filter_param()]) :: %{String.t() => String.t()}
defp filter(nil), do: %{}

defp filter(filters) do
Map.new(filters, fn {field, value} ->
{"filter[#{field}]", FilterValue.filter_value_string(value)}
end)
end
end
57 changes: 57 additions & 0 deletions lib/mbta_v3_api/route.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
defmodule MBTAV3API.Route do
alias MBTAV3API.JsonApi

@behaviour JsonApi.Object

@type t :: %__MODULE__{
id: String.t(),
color: String.t(),
direction_names: [String.t()],
direction_destinations: [String.t()],
long_name: String.t(),
short_name: String.t(),
sort_order: String.t(),
text_color: String.t()
}

@derive Jason.Encoder
defstruct [
:id,
:color,
:direction_names,
:direction_destinations,
:long_name,
:short_name,
:sort_order,
:text_color
]

@impl JsonApi.Object
def fields,
do: [
:color,
:direction_names,
:direction_destinations,
:long_name,
:short_name,
:sort_order,
:text_color
]

@impl JsonApi.Object
def includes, do: %{}

@spec parse(JsonApi.Item.t()) :: t()
def parse(%JsonApi.Item{} = item) do
%__MODULE__{
id: item.id,
color: item.attributes["color"],
direction_names: item.attributes["direction_names"],
direction_destinations: item.attributes["direction_destinations"],
long_name: item.attributes["long_name"],
short_name: item.attributes["short_name"],
sort_order: item.attributes["sort_order"],
text_color: item.attributes["text_color"]
}
end
end
Loading

0 comments on commit f1c401c

Please sign in to comment.