Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement RESTful API for stop page data #23

Merged
merged 7 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ config :logger, :console,
# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

config :jsonapi,
namespace: "/jsonapi",
field_transformation: :camelize,
remove_links: true

# 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"
36 changes: 20 additions & 16 deletions lib/mbta_v3_api/json_api.ex
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
defmodule JsonApi.Item do
defmodule MBTAV3API.JsonApi.Item do
@moduledoc """
JSON API results data.
"""
@derive Jason.Encoder

defstruct [:type, :id, :attributes, :relationships]

@type t :: %JsonApi.Item{
@type t :: %__MODULE__{
type: String.t(),
id: String.t(),
attributes: %{String.t() => any},
relationships: %{String.t() => list(JsonApi.Item.t())}
relationships: %{String.t() => list(t())}
}
end

defmodule JsonApi.Error do
defmodule MBTAV3API.JsonApi.Error do
@moduledoc """
JSON API error data.
"""
Expand All @@ -30,36 +30,40 @@ defmodule JsonApi.Error do
}
end

defmodule JsonApi do
defmodule MBTAV3API.JsonApi do
@moduledoc """
Helpers for working with a JSON API.
"""
@derive Jason.Encoder

defstruct links: %{}, data: []
@type t :: %JsonApi{links: %{String.t() => String.t()}, data: list(JsonApi.Item.t())}

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

@spec empty() :: t()
def empty do
%JsonApi{
%__MODULE__{
links: %{},
data: []
}
end

@spec merge(JsonApi.t(), JsonApi.t()) :: JsonApi.t()
@spec merge(t(), t()) :: t()
def merge(j1, j2) do
%JsonApi{
%__MODULE__{
links: Map.merge(j1.links, j2.links),
data: j1.data ++ j2.data
}
end

@spec parse(String.t()) :: JsonApi.t() | {:error, any}
@spec parse(String.t()) :: t() | {:error, any}
def parse(body) do
with {:ok, parsed} <- Jason.decode(body),
{:ok, data} <- parse_data(parsed) do
%JsonApi{
%__MODULE__{
links: parse_links(parsed),
data: data
}
Expand All @@ -83,7 +87,7 @@ defmodule JsonApi do
%{}
end

@spec parse_data(term()) :: {:ok, [JsonApi.Item.t()]} | {:error, any}
@spec parse_data(term()) :: {:ok, [MBTAV3API.JsonApi.Item.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 @@ -113,7 +117,7 @@ defmodule JsonApi do
end

def parse_data_item(%{"type" => type, "id" => id, "attributes" => attributes} = item, included) do
%JsonApi.Item{
%MBTAV3API.JsonApi.Item{
type: type,
id: id,
attributes: attributes,
Expand All @@ -122,7 +126,7 @@ defmodule JsonApi do
end

def parse_data_item(%{"type" => type, "id" => id}, _) do
%JsonApi.Item{
%MBTAV3API.JsonApi.Item{
type: type,
id: id
}
Expand Down Expand Up @@ -196,7 +200,7 @@ defmodule JsonApi do
end

defp parse_error(error) do
%JsonApi.Error{
%MBTAV3API.JsonApi.Error{
code: error["code"],
detail: error["detail"],
source: error["source"],
Expand Down
1 change: 1 addition & 0 deletions lib/mbta_v3_api/mbta_v3_api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule MBTAV3API do
use HTTPoison.Base
require Logger
alias MBTAV3API.Cache
alias MBTAV3API.JsonApi
alias Util

@spec get_json(String.t(), Keyword.t()) :: JsonApi.t() | {:error, any}
Expand Down
1 change: 1 addition & 0 deletions lib/mbta_v3_api/mbta_v3_api/routes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ defmodule MBTAV3API.Routes do
@moduledoc """
Responsible for fetching Route data from the V3 API.
"""
alias MBTAV3API.JsonApi
alias Routes.Route
alias Stops.Stop

Expand Down
2 changes: 1 addition & 1 deletion lib/mbta_v3_api/route_patterns/route_pattern.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ defmodule RoutePatterns.RoutePattern do
Junction branch and the other for the Stoughton branch.
"""

alias JsonApi.Item
alias MBTAV3API.JsonApi.Item
alias Routes.Route
alias Schedules.Trip
alias Stops.Stop
Expand Down
4 changes: 2 additions & 2 deletions lib/mbta_v3_api/routes/parser.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule Routes.Parser do
@moduledoc "Functions for parsing generic JSON:API structs into Routes structs."

alias JsonApi.Item
alias MBTAV3API.JsonApi.Item
alias RoutePatterns.RoutePattern
alias Routes.{Route, Shape}

Expand Down Expand Up @@ -108,7 +108,7 @@ defmodule Routes.Parser do
]
end

@spec stop_ids(%{String.t() => list(JsonApi.Item.t())}) :: [Stops.Stop.id_t()]
@spec stop_ids(%{String.t() => list(Item.t())}) :: [Stops.Stop.id_t()]
defp stop_ids(%{"stops" => stops}), do: Enum.map(stops, & &1.id)
defp stop_ids(_), do: []
end
2 changes: 1 addition & 1 deletion lib/mbta_v3_api/routes/repo.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule Routes.Repo do

import Routes.Parser

alias JsonApi
alias MBTAV3API.JsonApi
# alias Routes.{Route, Shape}
alias Routes.Route
# alias MBTAV3API.{Shapes}
Expand Down
3 changes: 2 additions & 1 deletion lib/mbta_v3_api/stops/api.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ defmodule Stops.Api do
Wrapper around the remote stop information service.
"""
require Logger
alias JsonApi.Item
alias MBTAV3API.JsonApi
alias MBTAV3API.JsonApi.Item
alias Stops.Stop

@type fare_facility ::
Expand Down
52 changes: 52 additions & 0 deletions lib/mix/tasks/mock_api.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule Mix.Tasks.MockApi do
require Logger
use Mix.Task
@shortdoc "Refresh mock API responses"
@requirements ["app.start"]

@urls %{
stops_boyls:
{"/stops/place-boyls",
"include=parent_station%2Cfacilities%2Cchild_stops&fields%5Bfacility%5D=long_name%2Ctype%2Cproperties%2Clatitude%2Clongitude%2Cid&fields%5Bstop%5D=address%2Cname%2Clatitude%2Clongitude%2Caddress%2Cmunicipality%2Cwheelchair_boarding%2Clocation_type%2Cplatform_name%2Cplatform_code%2Cdescription"},
routes_boyls: {"/routes/", "stop=place-boyls&include=route_patterns"}
}

def run(_) do
test_data_dir = Application.app_dir(:mobile_app_backend, ["priv", "test-data"])
File.mkdir_p!(test_data_dir)

url_paths =
for {name, {path, query}} <- @urls do
data = get!(path, query)

filename = "#{name}.json"

Path.join(test_data_dir, filename)
|> File.write!(data)

%{path => %{query => filename}}
end
|> Enum.reduce(fn m1, m2 -> Map.merge(m1, m2, fn _k, v1, v2 -> Map.merge(v1, v2) end) end)

Path.join(test_data_dir, "meta.json")
|> File.write!(Jason.encode_to_iodata!(url_paths, pretty: true))
end

defp get!(path, query) do
base_url = Application.fetch_env!(:mobile_app_backend, :base_url)
api_key = Application.fetch_env!(:mobile_app_backend, :api_key)

url =
URI.new!(base_url) |> URI.append_path(path) |> URI.append_query(query) |> URI.to_string()

Logger.info("Downloading #{url}")

%HTTPoison.Response{body: body, status_code: 200} =
EmmaSimon marked this conversation as resolved.
Show resolved Hide resolved
HTTPoison.get!(url, [
{"accept", "application/vnd.api+json"}
| MBTAV3API.Headers.build(api_key, use_cache?: false)
])

body
end
end
22 changes: 22 additions & 0 deletions lib/mobile_app_backend_web/controllers/route_pattern_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule MobileAppBackendWeb.RoutePatternView do
use JSONAPI.View, type: "routePattern"

def fields do
[
:direction_id,
:id,
:name,
:representative_trip_id,
:representative_trip_polyline,
:shape_id,
:shape_priority,
:headsign,
:stop_ids,
:route_id,
:time_desc,
:typicality,
:service_id,
:sort_order
]
end
end
32 changes: 32 additions & 0 deletions lib/mobile_app_backend_web/controllers/route_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule MobileAppBackendWeb.RouteView do
use JSONAPI.View, type: "route"

def fields do
[
:id,
:type,
:name,
:long_name,
:color,
:sort_order,
:direction_names,
:direction_destinations,
:description,
:fare_class,
:custom_route?,
:line_id
]
end

def direction_names(route, _conn) do
route.direction_names |> Map.new(fn {k, v} -> {"#{k}", v} end)
end

def direction_destinations(route, _conn) do
route.direction_destinations |> Map.new(fn {k, v} -> {"#{k}", v} end)
EmmaSimon marked this conversation as resolved.
Show resolved Hide resolved
end

def relationships do
[route_patterns: MobileAppBackendWeb.RoutePatternView]
end
end
21 changes: 21 additions & 0 deletions lib/mobile_app_backend_web/controllers/stop_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule MobileAppBackendWeb.StopController do
use MobileAppBackendWeb, :controller

plug(:put_view, MobileAppBackendWeb.StopView)

plug JSONAPI.QueryParser, view: MobileAppBackendWeb.StopView

def show(conn, %{"id" => stop_id}) do
stop_info = Stops.Repo.get!(stop_id)

routes =
Routes.Repo.by_stop_with_route_pattern(stop_id)
|> Enum.map(fn {route, route_patterns} ->
Map.put(route, :route_patterns, route_patterns)
end)
EmmaSimon marked this conversation as resolved.
Show resolved Hide resolved

stop_info = Map.put(stop_info, :routes, routes)

render(conn, "show.json", %{data: stop_info})
end
end
43 changes: 43 additions & 0 deletions lib/mobile_app_backend_web/controllers/stop_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
defmodule MobileAppBackendWeb.StopView do
use JSONAPI.View, type: "stop"

def fields do
[
:id,
:parent_id,
:child_ids,
:name,
:note,
:accessibility,
:address,
:municipality,
:parking_lots,
:fare_facilities,
:bike_storage,
:latitude,
:longitude,
:is_child?,
:station?,
:has_fare_machine?,
:has_charlie_card_vendor?,
:closed_stop_info,
:type,
:platform_name,
:platform_code,
:description,
:zone
]
end

def fare_facilities(stop, _conn) do
stop.fare_facilities |> Enum.to_list()
end

def bike_storage(stop, _conn) do
stop.bike_storage |> Enum.to_list()
end

def relationships do
[routes: MobileAppBackendWeb.RouteView]
end
end
12 changes: 12 additions & 0 deletions lib/mobile_app_backend_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ defmodule MobileAppBackendWeb.Router do
plug :accepts, ["json"]
end

pipeline :jsonapi do
plug JSONAPI.EnsureSpec
plug JSONAPI.Deserializer
plug JSONAPI.UnderscoreParameters
end

scope "/", MobileAppBackendWeb do
pipe_through :browser

Expand All @@ -31,6 +37,12 @@ defmodule MobileAppBackendWeb.Router do
get("/route/by-stop/:stop_id", RouteController, :by_stop)
end

scope "/jsonapi", MobileAppBackendWeb do
pipe_through :jsonapi

resources("/stop", StopController, only: [:show])
end

scope "/graphql" do
pipe_through :api

Expand Down
Loading
Loading