All checks were successful
Build and Deploy / Build & Push Image (push) Successful in 32s
143 lines
4.2 KiB
Elixir
143 lines
4.2 KiB
Elixir
defmodule CobblemonUi.EvolutionApi do
|
|
@moduledoc """
|
|
Fetches and caches Pokémon evolution data from PokeAPI.
|
|
|
|
Provides `get_evolutions/1` which returns the list of species that
|
|
a given Pokémon can evolve into (direct next-stage evolutions).
|
|
Results are cached in an ETS table to avoid repeated API calls.
|
|
"""
|
|
|
|
use GenServer
|
|
require Logger
|
|
|
|
@table :evolution_cache
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Client API
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def start_link(_opts) do
|
|
GenServer.start_link(__MODULE__, [], name: __MODULE__)
|
|
end
|
|
|
|
@doc """
|
|
Returns a list of species names (lowercase strings) that the given species
|
|
evolves into. Returns an empty list if there are no evolutions or the lookup
|
|
fails.
|
|
|
|
Results are cached after the first successful fetch.
|
|
"""
|
|
@spec get_evolutions(String.t()) :: [String.t()]
|
|
def get_evolutions(species) when is_binary(species) do
|
|
key = String.downcase(species)
|
|
|
|
case :ets.lookup(@table, key) do
|
|
[{^key, evolutions}] ->
|
|
evolutions
|
|
|
|
[] ->
|
|
GenServer.call(__MODULE__, {:fetch, key}, 15_000)
|
|
end
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Server callbacks
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@impl true
|
|
def init(_opts) do
|
|
:ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
|
|
{:ok, %{}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call({:fetch, species}, _from, state) do
|
|
# Double-check cache (another caller may have populated it)
|
|
result =
|
|
case :ets.lookup(@table, species) do
|
|
[{^species, evolutions}] ->
|
|
evolutions
|
|
|
|
[] ->
|
|
evolutions = fetch_evolutions(species)
|
|
:ets.insert(@table, {species, evolutions})
|
|
evolutions
|
|
end
|
|
|
|
{:reply, result, state}
|
|
end
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Private helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
defp fetch_evolutions(species) do
|
|
with {:ok, chain_url} <- fetch_species_chain_url(species),
|
|
{:ok, chain} <- fetch_chain(chain_url) do
|
|
find_next_evolutions(chain, species)
|
|
else
|
|
_ -> []
|
|
end
|
|
end
|
|
|
|
defp fetch_species_chain_url(species) do
|
|
api_name = CobblemonUi.PokeApiNames.normalize(species)
|
|
url = "https://pokeapi.co/api/v2/pokemon-species/#{api_name}"
|
|
|
|
case Req.get(url) do
|
|
{:ok, %Req.Response{status: 200, body: %{"evolution_chain" => %{"url" => url}}}} ->
|
|
{:ok, url}
|
|
|
|
{:ok, %Req.Response{status: status}} ->
|
|
Logger.warning("[EvolutionApi] Species lookup failed for #{api_name}: HTTP #{status}")
|
|
{:error, :not_found}
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("[EvolutionApi] Species lookup failed for #{api_name}: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
defp fetch_chain(url) do
|
|
case Req.get(url) do
|
|
{:ok, %Req.Response{status: 200, body: %{"chain" => chain}}} ->
|
|
{:ok, chain}
|
|
|
|
{:ok, %Req.Response{status: status}} ->
|
|
Logger.warning("[EvolutionApi] Chain fetch failed: HTTP #{status}")
|
|
{:error, :not_found}
|
|
|
|
{:error, reason} ->
|
|
Logger.warning("[EvolutionApi] Chain fetch failed: #{inspect(reason)}")
|
|
{:error, reason}
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
# Walks the evolution chain tree and returns the species names of
|
|
# the direct next-stage evolutions for the given species.
|
|
def find_next_evolutions(chain, target_species) do
|
|
target = String.downcase(target_species)
|
|
do_find(chain, target)
|
|
end
|
|
|
|
defp do_find(%{"species" => %{"name" => name}, "evolves_to" => evolves_to}, target) do
|
|
if String.downcase(name) == target do
|
|
# Found the target — return names of its direct evolutions
|
|
Enum.map(evolves_to, fn evo ->
|
|
get_in(evo, ["species", "name"]) |> String.downcase()
|
|
end)
|
|
else
|
|
# Recurse into each branch
|
|
Enum.find_value(evolves_to, [], fn evo ->
|
|
case do_find(evo, target) do
|
|
[] -> nil
|
|
result -> result
|
|
end
|
|
end)
|
|
end
|
|
end
|
|
|
|
defp do_find(_, _target), do: []
|
|
end
|