From 6a0bfdb8eb8d34e985b078193eaf11de2e8fdd5e Mon Sep 17 00:00:00 2001 From: Antoine Augusti Date: Wed, 23 Oct 2024 10:47:41 +0200 Subject: [PATCH] =?UTF-8?q?GBFSMetadata=20:=20g=C3=A8re=20noms=20pour=20fl?= =?UTF-8?q?ux=20vehicle=5Fstatus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/shared/lib/gbfs_metadata.ex | 68 +++++++++++++------ apps/shared/test/gbfs_metadata_test.exs | 39 +++++++++++ .../lib/transport/gbfs_to_geojson.ex | 26 +++---- 3 files changed, 102 insertions(+), 31 deletions(-) diff --git a/apps/shared/lib/gbfs_metadata.ex b/apps/shared/lib/gbfs_metadata.ex index f8db3a7b0e..f2ea66b033 100644 --- a/apps/shared/lib/gbfs_metadata.ex +++ b/apps/shared/lib/gbfs_metadata.ex @@ -17,6 +17,23 @@ defmodule Transport.Shared.GBFSMetadata do require Logger @behaviour Transport.Shared.GBFSMetadata.Wrapper + @type feed_name :: + :gbfs + | :manifest + | :gbfs_versions + | :system_information + | :vehicle_types + | :station_information + | :station_status + # `vehicle_status` was `free_bike_status` before v3.0 + | :vehicle_status + | :system_hours + | :system_calendar + | :system_regions + | :system_pricing_plans + | :system_alerts + | :geofencing_zones + @doc """ This function does 2 HTTP calls on a given resource url, and returns a report with metadata and also validation status (using a third-party HTTP validator). @@ -56,14 +73,14 @@ defmodule Transport.Shared.GBFSMetadata do end defp types(%{"data" => _data} = payload) do - has_bike_status = has_feed?(payload, "free_bike_status") - has_station_information = has_feed?(payload, "station_information") + has_vehicle_status = has_feed?(payload, :vehicle_status) + has_station_information = has_feed?(payload, :station_information) cond do - has_bike_status and has_station_information -> + has_vehicle_status and has_station_information -> ["free_floating", "stations"] - has_bike_status -> + has_vehicle_status -> ["free_floating"] has_station_information -> @@ -141,25 +158,26 @@ defmodule Transport.Shared.GBFSMetadata do Determines the feed to use as the ttl value of a GBFS feed. iex> Transport.Shared.GBFSMetadata.feed_to_use_for_ttl(["free_floating", "stations"]) - "free_bike_status" + :vehicle_status iex> Transport.Shared.GBFSMetadata.feed_to_use_for_ttl(["stations"]) - "station_information" + :station_information iex> Transport.Shared.GBFSMetadata.feed_to_use_for_ttl(nil) nil """ + @spec feed_to_use_for_ttl([binary()] | nil) :: feed_name() | nil def feed_to_use_for_ttl(types) do case types do - ["free_floating", "stations"] -> "free_bike_status" - ["free_floating"] -> "free_bike_status" - ["stations"] -> "station_information" + ["free_floating", "stations"] -> :vehicle_status + ["free_floating"] -> :vehicle_status + ["stations"] -> :station_information nil -> nil end end defp system_details(%{"data" => _data} = payload) do - feed_url = payload |> first_feed() |> feed_url_by_name("system_information") + feed_url = payload |> first_feed() |> feed_url_by_name(:system_information) if not is_nil(feed_url) do with {:ok, %{status_code: 200, body: body}} <- http_client().get(feed_url), @@ -186,7 +204,7 @@ defmodule Transport.Shared.GBFSMetadata do end def vehicle_types(%{"data" => _data} = payload) do - feed_url = payload |> first_feed() |> feed_url_by_name("vehicle_types") + feed_url = payload |> first_feed() |> feed_url_by_name(:vehicle_types) if is_nil(feed_url) do # https://gbfs.org/specification/reference/#vehicle_typesjson @@ -206,7 +224,7 @@ defmodule Transport.Shared.GBFSMetadata do if before_v3?(payload) do Map.keys(data) else - feed_url = payload |> first_feed() |> feed_url_by_name("system_information") + feed_url = payload |> first_feed() |> feed_url_by_name(:system_information) with {:ok, %HTTPoison.Response{status_code: 200, body: body}} <- http_client().get(feed_url), {:ok, json} <- Jason.decode(body) do @@ -219,7 +237,7 @@ defmodule Transport.Shared.GBFSMetadata do @spec versions(map()) :: [binary()] | nil def versions(%{"data" => _data} = payload) do - versions_url = payload |> first_feed() |> feed_url_by_name("gbfs_versions") + versions_url = payload |> first_feed() |> feed_url_by_name(:gbfs_versions) if is_nil(versions_url) do [Map.get(payload, "version", "1.0")] @@ -233,23 +251,35 @@ defmodule Transport.Shared.GBFSMetadata do end end - @spec feed_url_by_name(list(), binary()) :: binary() | nil + @spec feed_url_by_name(list(), feed_name()) :: binary() | nil def feed_url_by_name(feeds, name) do Enum.find(feeds, fn map -> feed_is_named?(map, name) end)["url"] end - @spec feed_is_named?(map(), binary()) :: boolean() - def feed_is_named?(map, name) do + @spec feed_is_named?(map(), feed_name()) :: boolean() + def feed_is_named?(%{"name" => feed_name}, name) do # Many people make the mistake of appending `.json` to feed names # so try to match this as well - Enum.member?([name, "#{name}.json"], map["name"]) + searches = [to_string(name), "#{name}.json"] + + if name == :vehicle_status do + searches ++ ["free_bike_status", "free_bike_status.json"] + else + searches + end + |> Enum.member?(feed_name) + end + + @spec has_feed?(map(), feed_name()) :: boolean() + def has_feed?(%{"data" => _data} = payload, :vehicle_status) do + not MapSet.disjoint?(MapSet.new(feeds(payload)), MapSet.new(["vehicle_status", "free_bike_status"])) end - @spec has_feed?(map(), binary()) :: boolean() def has_feed?(%{"data" => _data} = payload, name) do - Enum.member?(feeds(payload), name) + Enum.member?(feeds(payload), to_string(name)) end + @spec feeds(map()) :: [binary()] def feeds(%{"data" => _data} = payload) do # Remove potential ".json" at the end of feed names as people # often make this mistake diff --git a/apps/shared/test/gbfs_metadata_test.exs b/apps/shared/test/gbfs_metadata_test.exs index 22eafcd570..07b715230a 100644 --- a/apps/shared/test/gbfs_metadata_test.exs +++ b/apps/shared/test/gbfs_metadata_test.exs @@ -373,6 +373,45 @@ defmodule Transport.Shared.GBFSMetadataTest do end end + describe "free_bike_status becomes vehicle_status" do + test "v2.3" do + assert feed_is_named?(%{"name" => "free_bike_status"}, :vehicle_status) + assert feed_is_named?(%{"name" => "free_bike_status.json"}, :vehicle_status) + + payload = %{ + "version" => "2.3", + "data" => %{ + "en" => %{ + "feeds" => [%{"name" => "free_bike_status", "url" => feed_url = "https://example.com/free_bike_status"}] + } + } + } + + assert has_feed?(payload, :vehicle_status) + + assert payload |> first_feed() |> feed_url_by_name(:vehicle_status) == feed_url + + assert has_feed?( + %{"version" => "2.3", "data" => %{"en" => %{"feeds" => [%{"name" => "free_bike_status.json"}]}}}, + :vehicle_status + ) + end + + test "v3.0" do + assert feed_is_named?(%{"name" => "vehicle_status"}, :vehicle_status) + + payload = %{ + "version" => "3.0", + "data" => %{ + "feeds" => [%{"name" => "vehicle_status", "url" => feed_url = "https://example.com/free_bike_status"}] + } + } + + assert has_feed?(payload, :vehicle_status) + assert payload |> first_feed() |> feed_url_by_name(:vehicle_status) == feed_url + end + end + defp setup_validation_result(summary \\ nil) do Shared.Validation.GBFSValidator.Mock |> expect(:validate, fn url -> diff --git a/apps/transport/lib/transport/gbfs_to_geojson.ex b/apps/transport/lib/transport/gbfs_to_geojson.ex index 315e56cd34..4178e5ba7d 100644 --- a/apps/transport/lib/transport/gbfs_to_geojson.ex +++ b/apps/transport/lib/transport/gbfs_to_geojson.ex @@ -4,6 +4,8 @@ defmodule Transport.GbfsToGeojson do """ alias Transport.Shared.GBFSMetadata + @type feed_name :: Transport.Shared.GBFSMetadata.feed_name() + @doc """ Main module function: returns a map of geojsons generated from the GBFS endpoint %{ @@ -29,7 +31,7 @@ defmodule Transport.GbfsToGeojson do def add_feeds(payload, %{"output" => "free_floating"}) do %{} - |> add_free_bike_status(payload) + |> add_vehicle_status(payload) |> Map.get("free_floating") end @@ -43,7 +45,7 @@ defmodule Transport.GbfsToGeojson do %{} |> add_station_information(payload) |> add_station_status(payload) - |> add_free_bike_status(payload) + |> add_vehicle_status(payload) |> add_geofencing_zones(payload) rescue _e -> %{} @@ -52,7 +54,7 @@ defmodule Transport.GbfsToGeojson do @spec add_station_information(map(), map()) :: map() defp add_station_information(resp_data, payload) do payload - |> feed_url_from_payload("station_information") + |> feed_url_from_payload(:station_information) |> case do nil -> resp_data @@ -103,7 +105,7 @@ defmodule Transport.GbfsToGeojson do @spec add_station_status(map(), map()) :: map() defp add_station_status(%{"stations" => stations_geojson} = resp_data, payload) do payload - |> feed_url_from_payload("station_status") + |> feed_url_from_payload(:station_status) |> case do nil -> resp_data @@ -155,24 +157,24 @@ defmodule Transport.GbfsToGeojson do Map.put(data, "availability", availability) end - @spec add_free_bike_status(map(), map()) :: map() - defp add_free_bike_status(resp_data, payload) do + @spec add_vehicle_status(map(), map()) :: map() + defp add_vehicle_status(resp_data, payload) do payload - |> feed_url_from_payload("free_bike_status") + |> feed_url_from_payload(:vehicle_status) |> case do nil -> resp_data url -> - geojson = free_bike_status_geojson!(url) + geojson = vehicle_status_geojson!(url) resp_data |> Map.put("free_floating", geojson) end rescue _e -> resp_data end - @spec free_bike_status_geojson!(binary()) :: map() - defp free_bike_status_geojson!(url) do + @spec vehicle_status_geojson!(binary()) :: map() + defp vehicle_status_geojson!(url) do json = fetch_gbfs_endpoint!(url) vehicles = @@ -203,7 +205,7 @@ defmodule Transport.GbfsToGeojson do @spec add_geofencing_zones(map(), map()) :: map() defp add_geofencing_zones(resp_data, payload) do payload - |> feed_url_from_payload("geofencing_zones") + |> feed_url_from_payload(:geofencing_zones) |> case do nil -> resp_data @@ -230,7 +232,7 @@ defmodule Transport.GbfsToGeojson do Jason.decode!(body) end - @spec feed_url_from_payload(map(), binary()) :: binary() | nil + @spec feed_url_from_payload(map(), feed_name()) :: binary() | nil defp feed_url_from_payload(payload, feed_name) do payload |> GBFSMetadata.first_feed() |> GBFSMetadata.feed_url_by_name(feed_name) end